Skip to content

feat(core): merge architecture improvements into main - #10

Merged
mcocdaa merged 313 commits into
mainfrom
feat/architecture
Sep 24, 2026
Merged

mcocdaa merged 313 commits into
mainfrom
feat/architecture

Conversation

@mcocdaa

@mcocdaa mcocdaa commented Apr 26, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

合并 feat/architecture 分支到 main,包含 Core 模块的大规模架构重构与缺陷修复。

Changes

  • feat(Flywheel): 全链路数据飞轮 - Monitor/Execute/Plan 三层架构
  • feat(Flywheel): 存储路径可配置 + Embedding 快照导出 + 分析报告持久化 + AutoApplyMode
  • feat(Settings): 新增可配置 defaultTemperature
  • fix: AgentBus 线程安全(ConcurrentDictionary)、死代码清理
  • fix: 确保所有异步回调在主线程执行
  • fix: Npc ChatAsync 超时、AgentDialogue AI 响应更新
  • fix: Perception patch 性能、事件过滤、内存泄漏清理
  • fix: ContextDiff tick-based 过期、PawnAgent 健壮性
  • fix: Flywheel 序列化、空 catch、硬编码参数、PatchMood、PerceptionBuffer Queue
  • fix: P2 中等问题 - 缓存污染、冗余代码、fallback 日志
  • docs: 重写 AGENTS.md、INTRODUCTION.txt、README.md
  • chore: 更新 debug log defs 和本地化

Commits

  • 3866a3b fix(AgentBus): use ConcurrentDictionary for thread safety, remove dead code
  • 920b342 fix(threading): ensure all async callbacks execute on main thread
  • f1c7269 fix(Npc): add ChatAsync timeout, update AgentDialogue on AI response
  • 04b9cac fix(Perception): patch performance, event filtering, memory leak cleanup
  • 30e32c4 fix(Npc): serialize TtsConfig, publish lifecycle event on KillNpc
  • 387b576 fix(Agent): ContextDiff tick-based expiry, PawnAgent robustness improvements
  • ea37a4b fix(Npc): Player2StorageDriver health check and error logging
  • 146dcbc fix(Flywheel): compile errors - GameComponent ctor, LoadSaveMode, GenFilePaths.SaveDataFolderPath
  • 8cb9786 refactor: remove dead code across multiple files
  • 67aca23 feat(Settings): add configurable defaultTemperature
  • 99ea4b4 fix: P2 medium issues — cache pollution, redundant code, fallback logging
  • b93ffb4 feat(Flywheel): full-chain data flywheel - Monitor/Execute/Plan layers
  • 96fb37d chore(core): update debug log defs and localizations
  • 912ec8f feat(Flywheel): storage path configurable + Embedding snapshot export + analysis report persistence + AutoApplyMode
  • f9629ff docs: 重写 AGENTS.md、INTRODUCTION.txt、README.md
  • e07f1bd fix: Flywheel serialization, empty catches, hardcoded params, PatchMood, PerceptionBuffer Queue
  • 4af8e7b fix something
  • fda4254 fix

Checklist

  • 代码编译通过
  • 功能测试完成
  • 文档已更新

Summary by CodeRabbit

发布说明

  • 新功能

    • 新增可与角色对话的 NPC 对话窗口与实时对话历史
    • 引入代理系统:个体目标、行为记录、感知与决策循环
    • 支持结构化 AI 输出与工具调用、传感器驱动的工具集成
    • 增加 Player2 远程集成与混合本地/远程存储驱动
    • 添加音频播放与命令分发机制
  • 改进

    • 全面上下文/记忆管理与嵌入缓存,提升生成质量与效率
    • 新的自动参数调优与遥测采集(Flywheel)与可配置自动应用
    • 扩展设置界面(温度、Player2 URL、预算与飞轮选项)与多语言文案更新
  • 调试/工具

    • 增强调试视图、事件总线与丰富的诊断命令

@coderabbitai

coderabbitai Bot commented Apr 26, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Repository: RimWorld-RimMind-Mod/RimWorld-RimMind-Mod-Core/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 67842797-1b52-4f55-9b13-2358479434f6

Walkthrough

引入完整的NPC代理子系统、分层上下文引擎、感知管道、事件总线、飞轮遥测/规则引擎、嵌入与历史管理、多个存储驱动和大量公共API扩展;并更新本地化、文档与设置 UI。

Changes

Cohort / File(s) Summary
配置与文档
/.coderabbit.yaml, About/About.xml, About/PublishedFileId.txt, Defs/MainButtons/AICore_DebugLog.xml, INTRODUCTION.txt, Languages/ChineseSimplified/Keyed/RimMind_Core.xml, Languages/English/Keyed/RimMind_Core.xml
尾部换行/缩进修复;重写/扩展简介和英中本地化键以支持新UI、设置与大量提示模板。
公共客户端与协议
Source/Client/..., Source/Client/OpenAI/..., Source/Client/Player2/..., Source/Client/JsonRepairHelper.cs, Source/Client/QuotaExceededException.cs, Source/Client/StructuredTool.cs
扩展 AI 消息/响应以支持推理内容、工具调用、分词统计;新增结构化请求 API、响应格式回退与格式能⼒缓存、JSON 修复与配额异常类型;Player2 客户端增添结构化/余额/原始请求支持。
AICore / API 层
Source/AICoreAPI.cs, Source/Client/IAIClient.cs, Source/Client/AIRequest.cs, Source/Client/AIResponse.cs
重构请求入口以基于 ContextSnapshot,新增 RequestStructured API、提供者注册并发字典、事件/代理/流处理注册点及遥测记录。
代理核心(Agent)
Source/Core/Agent/..., Source/Comps/CompPawnAgent.cs
添加 PawnAgent、目标/目标栈、身份、状态、行为记录、策略优化器、感知缓冲与过滤器、感知管道、Agent 组件与相关提供者/桥接接口。
事件总线(AgentBus)
Source/Core/AgentBus/...
新增类型化事件总线、事件基类与多种事件类型(感知、决策、行动、目标、内存、生命周期)及 IEventBus 适配器。
上下文引擎与嵌入
Source/Core/Context/...
新增 ContextEngine、KeyMeta/注册表、预算调度器(BudgetScheduler 与配置)、场景/相关性/嵌入系统、快照/差异/历史管理、嵌入缓存与快照持久化。
飞轮与遥测
Source/Core/Flywheel/...
新增参数存储、遥测收集器、规则引擎、内置调优器、嵌入快照存储与报告写入、GameComponent 集成与自动应用控制。
NPC 存储与管理
Source/Npc/...
新增 IStorageDriver 接口及三种实现(Local/Player2/Hybrid)、NpcManager、NpcProfile、构建器、MapNpcComponent、响应分发器与存储驱动工厂。
感知补丁与传递
Source/Patch/..., Source/Core/Perception/PerceptionBridge.cs
新增 PerceptionBridge 与多处 Harmony 补丁以捕获击倒、伤害、心情、精神崩溃、袭击事件并发布感知;补丁、桥接与广播逻辑。
提示与构建工具
Source/Core/Prompt/...
PromptBudget、PromptSection、ContextComposer 与新的 TaskInstructionBuilder;删除旧的 StructuredPromptBuilder;分层标签与克隆支持。
UI 与调试
Source/UI/..., Source/Debug/AICoreDebugActions.cs
新增 Agent 对话窗口、AIDebugLog 右侧双窗格分割、设置界面增加 Player2/温度/飞轮与上下文预算控件;大量调试动作扩展。
设置与扩展点
Source/Settings/..., Source/Core/Extensions/...
新增设置字段(Player2 URL、temperature、context diff/校准间隔、遥测路径、自动应用阈值等);新增扩展接口(传感器、参数调优、音频、流式响应、代理模式等)。

Sequence Diagram(s)

sequenceDiagram
    participant UI as UI/Window_AgentDialogue
    participant Agent as PawnAgent
    participant Context as ContextEngine
    participant Scheduler as BudgetScheduler
    participant Storage as StorageDriver
    participant LLM as OpenAI/Player2 Client

    UI->>Agent: 用户输入 Send()
    Agent->>Context: BuildSnapshot(ContextRequest)
    Context->>Scheduler: Schedule(keys, budget)
    Scheduler-->>Context: 分层键集合 (L0..L5)
    Context-->>Agent: ContextSnapshot (Messages, Tools, tokens)
    Agent->>Storage: ChatAsync(snapshot)
    Storage->>LLM: SendStructuredAsync(request/json_schema/tools)
    LLM-->>Storage: AIResponse (content, toolCalls, tokens)
    Storage-->>Agent: NpcChatResult (message, commands, audio)
    Agent->>Agent: 解析决策/工具调用,执行桥接动作
    Agent-->>UI: 更新历史与显示响应
Loading
sequenceDiagram
    participant World as 游戏事件
    participant Patch as HarmonyPatch
    participant Bridge as PerceptionBridge
    participant Bus as AgentBus
    participant PawnAgent as PawnAgent
    participant GoalGen as GoalGenerator

    World->>Patch: 触发事件(伤害/击倒/心情/崩溃/袭击)
    Patch->>Bridge: PublishPerceptionForPawn(...)
    Bridge->>Bus: Publish(PerceptionEvent)
    Bus->>PawnAgent: 事件分发
    PawnAgent->>PawnAgent: 入队感知 -> 过滤管道 -> 缓冲
    PawnAgent->>PawnAgent: 达到触发条件 -> Think()
    PawnAgent->>GoalGen: GenerateFromState/Event()
    GoalGen-->>PawnAgent: 返回候选目标
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 分钟

Possibly related PRs

Suggested labels

needs-review, changes-requested

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/architecture

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
Source/Core/AIRequestQueue.cs (1)

63-93: ⚠️ Potential issue | 🔴 Critical

取消逻辑会与后台未完成的请求竞争,导致回调被触发两次或被取消的请求被重新入队

StartedNewGame() 与 LoadedGame() 仅向 _results 投递了 Cancelled 响应并清空了 _activeRequests / _modQueues,但 FireRequest 中通过 Task.Run 启动的后台 HTTP 调用并未取消。当该后台任务在新游戏开始/加载完成后才返回时:

  • 它仍会把结果推入 _pendingFireResults;
  • 下一次 GameComponentTick 调用 ProcessFireResult 时,_activeRequests.Remove(...) 是 no-op,但 Complete 分支会再次把响应入队 _results,导致同一个 RequestId 的回调被调用两次(一次 Cancelled、一次真实结果);
  • 若结果命中 Retry 分支,还会把已被“取消”的请求重新插回 _modQueues,相当于在新游戏里复活了上一局的请求。

建议记录已取消的 TrackingId(或在 TrackedRequest 上加个 Cancelled 标志),并在 ProcessFireResult 开头判断是否已取消,丢弃后续结果。

🔧 建议修复方向
+        private readonly HashSet<int> _cancelledTrackingIds = new HashSet<int>();
+
         public override void StartedNewGame()
         {
             foreach (var kvp in _activeRequests)
             {
                 var response = AIResponse.Cancelled(kvp.Value.Request.RequestId, "New game started, request cancelled");
                 response.Priority = kvp.Value.Request.Priority;
                 _results.Enqueue((response, kvp.Value.Callback));
+                _cancelledTrackingIds.Add(kvp.Value.TrackingId);
             }
             ...
         }
@@
         private void ProcessFireResult(PendingFireResult result)
         {
             var tracked = result.Tracked;
+            if (_cancelledTrackingIds.Remove(tracked.TrackingId))
+            {
+                // Request was cancelled by game start/load; drop the late result.
+                return;
+            }
             _activeRequests.Remove(tracked.TrackingId);

LoadedGame() 同理。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/AIRequestQueue.cs` around lines 63 - 93, The cancel logic
currently enqueues AIResponse.Cancelled and clears collections in StartedNewGame
and LoadedGame but doesn't stop background FireRequest tasks, so their results
still reach ProcessFireResult and can re-enqueue or retry cancelled requests;
fix by adding a cancelled marker (e.g. a bool Cancelled flag on TrackedRequest
or a HashSet of cancelled TrackingIds maintained when StartedNewGame/LoadedGame
run and cleared appropriately) and update ProcessFireResult to check that marker
at the start and drop any results for cancelled requests (including preventing
Retry branch from re-enqueuing into _modQueues or _results); ensure
FireRequest/Task results still call ProcessFireResult but get ignored for
cancelled TrackingIds to avoid duplicate callbacks.
Source/UI/Window_AIDebugLog.cs (1)

248-251: ⚠️ Potential issue | 🟡 Minor

过滤器对 FullResponse 未做空值保护,可能触发 NullReferenceException。

文件其它位置(Lines 149‑163、179‑196)都把 FullSystemPrompt / FullUserPrompt / FullResponse 当作可能为 null 来防御处理,但此处直接调用 .Contains(_filter)。一旦某条 AIDebugEntry 的 FullResponse(或 Source)为 null(例如错误请求、超时、被拦截的请求),用户在搜索框输入任意字符就会让整个调试窗口崩出 NRE。

🐛 建议的修复
-            return ((IEnumerable<AIDebugEntry>)all)
-                .Where(e => e.Source.Contains(_filter) || e.FullResponse.Contains(_filter))
-                .Reverse()
-                .ToList();
+            return ((IEnumerable<AIDebugEntry>)all)
+                .Where(e => (e.Source?.Contains(_filter) ?? false)
+                         || (e.FullResponse?.Contains(_filter) ?? false))
+                .Reverse()
+                .ToList();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/UI/Window_AIDebugLog.cs` around lines 248 - 251, The Where predicate
in the method returning IEnumerable<AIDebugEntry> calls
e.Source.Contains(_filter) and e.FullResponse.Contains(_filter) without null
checks which can throw NullReferenceException; update the predicate in
Window_AIDebugLog (the LINQ chain that uses all.Where(...).Reverse().ToList())
to guard against nulls by checking e.Source != null before calling Contains and
e.FullResponse != null before calling Contains (or use safe-evaluation like
checking .Contains only when the property is not null) so entries with null
Source/FullResponse are skipped rather than causing an exception.
Source/AICoreAPI.cs (1)

711-719: ⚠️ Potential issue | 🟠 Major

EnsurePlayer2Client 现在仅返回缓存,导致 Player2 的 RequestImmediate 与 GetClient 无法初始化客户端

EnsurePlayer2Client(第 711-719 行)已变更为纯缓存查询,不再创建 Player2Client 实例。这打破了以下调用路径:

  • LocalStorageDriver.cs:63 中的 GetClient() 调用 - 首次使用时返回 null
  • AICoreDebugActions.cs:41 中的 RequestImmediate() - 对 Player2 永远失败

客户端初始化现被限制在 RequestStructuredAsync 的异步 fallback 分支(第 157 行的 Player2Client.CreateAsync),这意味着用户必须先调用结构化请求才能触发缓存初始化,否则立即请求会失败。

需要明确此设计是否有意为之。若不是,建议在 EnsurePlayer2Client 中补充同步/异步创建逻辑,或在 Mod 启动时预初始化 _cachedPlayer2Client。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/AICoreAPI.cs` around lines 711 - 719, EnsurePlayer2Client currently
only reads the cache and never initializes Player2 clients, causing
GetClient()/RequestImmediate() to return null; change EnsurePlayer2Client to
initialize the client when missing for AIProvider.Player2 by creating or
starting initialization of _cachedPlayer2Client (restore the prior
synchronous/initialization behavior) so immediate requests work: detect when
_cachedPlayer2Client is null and _cachedProvider == AIProvider.Player2, then
either create a new Player2Client synchronously (or start
Player2Client.CreateAsync and block/await short-term or store a initializing
placeholder) and assign to _cachedPlayer2Client before returning, ensuring
IsConfigured() is still checked; this restores the initialization path used
previously and avoids relying solely on RequestStructuredAsync to populate the
cache.
🟠 Major comments (27)
Source/Client/JsonRepairHelper.cs-13-25 (1)

13-25: ⚠️ Potential issue | 🟠 Major

转义检测对 \\ 失效,会导致 inString 状态错误。

当前逻辑用 prev != '\\' 判断引号是否被转义,但在合法 JSON 字符串 "\\"(值为单个反斜杠)中,闭合引号前一个字符确实是 \,却是被前一个 \ 转义的反斜杠,引号本身并未被转义。此时该实现会误判为转义引号、跳过状态切换,使后续整个文档都被当作字符串内部,从而把真正的 {/}/[/] 当成字符串字符忽略,最终修复结果完全错误。

应使用一个跟踪“前一字符是否为未配对的反斜杠”的有状态布尔,而不是只看 prev。

🐛 建议修复
-            int openBraces = 0, openBrackets = 0;
-            bool inString = false;
-            char prev = '\0';
-            foreach (char c in trimmed)
-            {
-                if (prev != '\\' && c == '"') inString = !inString;
-                if (!inString)
-                {
-                    if (c == '{') openBraces++;
-                    else if (c == '}') openBraces--;
-                    else if (c == '[') openBrackets++;
-                    else if (c == ']') openBrackets--;
-                }
-                prev = c;
-            }
+            int openBraces = 0, openBrackets = 0;
+            bool inString = false;
+            bool escaped = false;
+            foreach (char c in trimmed)
+            {
+                if (inString)
+                {
+                    if (escaped) { escaped = false; }
+                    else if (c == '\\') { escaped = true; }
+                    else if (c == '"') { inString = false; }
+                    continue;
+                }
+                if (c == '"') { inString = true; continue; }
+                if (c == '{') openBraces++;
+                else if (c == '}') openBraces--;
+                else if (c == '[') openBrackets++;
+                else if (c == ']') openBrackets--;
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Client/JsonRepairHelper.cs` around lines 13 - 25, The loop in
JsonRepairHelper.cs that toggles inString using prev != '\\' is wrong for
sequences like "\\"; replace the prev-based check with an "escaped" boolean that
tracks whether the current character is escaped (toggle escaped when you see a
backslash while inString, reset escaped to false for any other character), and
use if (!escaped && c == '"') inString = !inString; keep the existing
brace/bracket counting (openBraces/openBrackets) unchanged and remove reliance
on prev (or stop updating prev) so escape handling is correct across runs.
Source/Client/JsonRepairHelper.cs-27-37 (1)

27-37: ⚠️ Potential issue | 🟠 Major

截断未闭合字符串后会留下悬空的 : 或元素分隔符,拼出非法 JSON。

当 inString == true 时,LastIndexOf('"') 实际定位到的是该未闭合字符串的起始引号。直接 Substring(0, lastQuote) 会得到形如 {"key": 或 [1, 的残余串,之后只 TrimEnd(',') 一次,无法清理冒号、空白以及对象/数组中已经写出的 key 部分;最终拼上 } / ] 会产出 {"key": }、[1, ] 这类仍然非法的 JSON,反而让下游解析器更难识别。

建议在截断之后把尾部的空白、,、: 一并剥掉;如果是对象上下文,还需要把悬空的 "key" 也回退掉,例如循环 TrimEnd 直到结尾是 {、[、, 或合法值字符为止,再决定是否需要去掉一个紧邻的 ,。

🐛 建议修复思路
             if (inString)
             {
                 int lastQuote = trimmed.LastIndexOf('"');
                 if (lastQuote > 0)
                     trimmed = trimmed.Substring(0, lastQuote);
             }
 
-            if (trimmed.EndsWith(",")) trimmed = trimmed.TrimEnd(',');
+            // 反复清理因截断暴露出的悬空分隔符 / key
+            while (trimmed.Length > 0)
+            {
+                char tail = trimmed[trimmed.Length - 1];
+                if (char.IsWhiteSpace(tail) || tail == ',' || tail == ':')
+                {
+                    trimmed = trimmed.Substring(0, trimmed.Length - 1);
+                    continue;
+                }
+                // 如果尾部是一个完整的 "key"(对象 key 还没等到 value),把它一起去掉
+                if (tail == '"')
+                {
+                    int prevQuote = trimmed.LastIndexOf('"', trimmed.Length - 2);
+                    if (prevQuote >= 0)
+                    {
+                        trimmed = trimmed.Substring(0, prevQuote);
+                        continue;
+                    }
+                }
+                break;
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Client/JsonRepairHelper.cs` around lines 27 - 37,
截断未闭合字符串时直接截到起始引号会留下悬空的冒号或逗号导致非法 JSON;在 JsonRepairHelper.cs 的 inString 分支(使用
LastIndexOf('"') 和 Substring 对 trimmed 截断)之后,改为:先循环剥掉尾部空白,然后循环剥掉尾部的 ',' 和
':',如果剥掉后尾字符是 '"'(表示留下了悬空的 key),再用 LastIndexOf('"') 找到前一个引号并 Substring
回退以删除整个键字符串;最后再按现有逻辑根据 openBraces/openBrackets 补 '}'/']'。确保使用 trimmed 的
TrimEnd/循环判断而不是只调用一次 TrimEnd(',')。
Source/Core/Context/RelevanceTable.cs-8-53 (1)

8-53: ⚠️ Potential issue | 🟠 Major

静态可变状态缺乏线程安全保护。

_table 与 _coreRegistered 都是进程范围的可变静态状态,但当前没有任何同步原语。考虑到 Context 系统通过 BuildSnapshot() 在异步方法中被调用(例如 LocalStorageDriver.ChatAsync 和 Player2StorageDriver.ChatAsync),这些异步调用可能在线程池线程上执行,导致 Register/RegisterBatch/Unregister* 与 GetRelevance 并发执行时会触发 Dictionary 的非线程安全行为(可能抛 InvalidOperationException 或读到撕裂值)。另外 Lines 52-53 的 if (_coreRegistered) return; _coreRegistered = true; 是经典 TOCTOU,理论上可被多个并发线程同时通过检查,导致 RegisterBatch 被重复执行。

🔒 建议改为 `ConcurrentDictionary` 并使用 `Interlocked` 守卫一次性初始化
-        private static readonly Dictionary<(string, string), float> _table = new Dictionary<(string, string), float>();
-        private static bool _coreRegistered = false;
+        private static readonly ConcurrentDictionary<(string, string), float> _table = new ConcurrentDictionary<(string, string), float>();
+        private static int _coreRegistered = 0;
         ...
-        public static void RegisterCoreRelevance()
-        {
-            if (_coreRegistered) return;
-            _coreRegistered = true;
+        public static void RegisterCoreRelevance()
+        {
+            if (Interlocked.Exchange(ref _coreRegistered, 1) == 1) return;

并相应调整 Unregister*/Clear 使用 TryRemove。

基于学习:AgentBus 采用 ConcurrentDictionary 实现线程安全订阅——同样的并发模型也适用于此处共享的注册表。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Context/RelevanceTable.cs` around lines 8 - 53, The static _table
and _coreRegistered are not thread-safe; replace the Dictionary<(string,string),
float> _table with a ConcurrentDictionary<(string,string), float> and update
Register, RegisterBatch, Unregister, UnregisterScenario and GetRelevance to use
ConcurrentDictionary APIs (TryAdd/ indexer / TryRemove / TryGetValue) to avoid
concurrent mutation issues, and change the bool _coreRegistered to an int
_coreRegisteredFlag and use Interlocked.CompareExchange in RegisterCoreRelevance
to perform one-time initialization (guarding the RegisterBatch calls) to
eliminate the TOCTOU race.
Source/Npc/Patch_MapNpcLifecycle.cs-10-19 (1)

10-19: ⚠️ Potential issue | 🟠 Major

将 Patch 文件移至正确目录并添加 Harmony ID

该 Patch 存在多个问题:

  1. 文件位置违规:Patch 文件应位于 Source/Patch/ 目录,而非 Source/Npc/。代码库中所有其他 Harmony patch 均遵循此约定(如 Source/Patch/AITogglePatch.cs 等)。

  2. 缺少 Harmony ID:根据编码指南,所有 Harmony patch 必须包含属性 [HarmonyID("mcocdaa.RimMindCore")]。

  3. Patch 可能冗余:MapNpcComponent 是 public 非抽象类且继承自 MapComponent,RimWorld 的 MapGenerator.GenerateMap() 内部调用的 ConstructComponents() 会通过反射自动发现并添加所有符合条件的子类。该 Patch 中的组件检查与手动添加应该是不必要的。

建议:将文件移至 Source/Patch/ 并添加缺失的 [HarmonyID] 属性;同时确认是否可以移除该 Patch,让组件通过 RimWorld 的标准自动发现机制添加。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Npc/Patch_MapNpcLifecycle.cs` around lines 10 - 19, Patch file
Patch_MapNpcLifecycle.cs should be moved from the Npc folder into the canonical
Patch folder and must include the Harmony ID attribute; open the patch class
that contains the Postfix(Map __result) method and add
[HarmonyID("mcocdaa.RimMindCore")] above the class declaration, then relocate
the file into Source/Patch/ to match other Harmony patches (e.g.,
AITogglePatch). Also evaluate removing the entire Postfix/patch for
MapNpcComponent since MapNpcComponent is public, non-abstract and derives from
MapComponent so RimWorld's ConstructComponents()/reflection will auto-add it; if
you keep the patch, ensure the logic referencing MapNpcComponent and
__result.components.Add(...) is truly necessary and documented.
Source/Client/AIResponse.cs-9-23 (1)

9-23: 🛠️ Refactor suggestion | 🟠 Major

新增成员使用公共字段而非属性,破坏了 AIResponse 公共 DTO 的一致性

AIResponse 现有成员均为 { get; set; } 属性,新增的 PromptTokens/CompletionTokens/CachedTokens/ToolCallsJson/ReasoningContent 却是裸字段。这一不一致会带来:

  • Newtonsoft.Json 默认对字段与属性的序列化行为可能不一致(取决于 MemberSerialization 配置);
  • 任何需要把 AIResponse 投影到接口、数据绑定或反射工具的下游代码都会因为这些字段不可见或可见方式不同而出问题;
  • 未来想加 init/private set 时需要破坏性修改。

建议统一为属性。

♻️ 建议修改
-        public int PromptTokens;
-        public int CompletionTokens;
-        public int CachedTokens;
+        public int PromptTokens { get; set; }
+        public int CompletionTokens { get; set; }
+        public int CachedTokens { get; set; }
@@
-        public string? ToolCallsJson;
-        public string? ReasoningContent;
+        public string? ToolCallsJson { get; set; }
+        public string? ReasoningContent { get; set; }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Client/AIResponse.cs` around lines 9 - 23, The new members on
AIResponse (PromptTokens, CompletionTokens, CachedTokens, ToolCallsJson,
ReasoningContent) were added as public fields which breaks the DTO's
property-based consistency; change each to be an auto-property (e.g., public int
PromptTokens { get; set; }) and for the nullable string members use nullable
auto-properties (e.g., public string? ToolCallsJson { get; set; }) with any
required default values to match surrounding members, ensuring all existing code
that expects properties, JSON serialization, and future accessors work
consistently with the AIResponse class.
Source/Core/Context/HistoryGameComponent.cs-20-25 (1)

20-25: ⚠️ Potential issue | 🟠 Major

Dictionary<string, List<HistoryEntry>> 配合 LookMode.Deep 无法正确序列化,必须改写。

Scribe_Collections.Look<K,V>(...) 在 LookMode.Deep 模式下,要求字典的值类型 V 实现 IExposable。这里 V = List<HistoryEntry>,而 List<T> 本身并不实现 IExposable,因此序列化时不会正确保存嵌套列表的内容;读档时该字典会被重建,但所有内部列表均为空,导致所有历史记录被静默丢失。

需要为 NPC 历史列表创建一个 IExposable 包装类:

推荐的解决方案
public sealed class HistoryEntryList : IExposable
{
    public List<HistoryEntry> Items = new List<HistoryEntry>();
    public HistoryEntryList() { }
    public HistoryEntryList(List<HistoryEntry> items) { Items = items ?? new List<HistoryEntry>(); }
    public void ExposeData() => Scribe_Collections.Look(ref Items, "items", LookMode.Deep);
}

然后在 HistoryGameComponent.ExposeData() 中调整:

  • _histories 类型改为 Dictionary<string, HistoryEntryList>
  • Saving 阶段:_histories = HistoryManager.Instance.GetAllForSave().ToDictionary(kv => kv.Key, kv => new HistoryEntryList(kv.Value))
  • Loading 阶段:HistoryManager.Instance.LoadFromSave(_histories.ToDictionary(kv => kv.Key, kv => kv.Value.Items))

另外,第 10 行的无参构造函数 HistoryGameComponent() 在 RimWorld 1.6+ 中由 Activator.CreateInstance 自动调用时总是传入 Game 参数,该无参版本为死代码,可删除。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Context/HistoryGameComponent.cs` around lines 20 - 25, The
dictionary _histories in HistoryGameComponent is using Dictionary<string,
List<HistoryEntry>> with Scribe_Collections.Look and LookMode.Deep which fails
because List<T> does not implement IExposable; create a new IExposable wrapper
class HistoryEntryList (with a public List<HistoryEntry> Items and ExposeData
that Scribe_Collections.Look(ref Items, "items", LookMode.Deep)) and change
_histories to Dictionary<string, HistoryEntryList>; update ExposeData to convert
between HistoryManager.Instance.GetAllForSave() and Dictionary<string,
HistoryEntryList> when saving and convert back to Dictionary<string,
List<HistoryEntry>> (kv.Value.Items) when loading via
HistoryManager.Instance.LoadFromSave; finally remove the unused parameterless
HistoryGameComponent() constructor as it’s redundant under RimWorld 1.6+
activation.
Source/UI/Window_AgentDialogue.cs-117-126 (1)

117-126: ⚠️ Potential issue | 🟠 Major

滚动条每帧被强制拉到底部,用户无法向上回看历史。

只要 _lastContentHeight > rect.height,_scrollPosition.y 每次 DoWindowContents 都会被重置到末尾。建议仅在历史条目数量增加(如 currentCount != _lastHistoryCount)时自动滚到底部,平时尊重用户的滚动位置。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/UI/Window_AgentDialogue.cs` around lines 117 - 126, The window always
forces scroll to bottom because DoWindowContents resets _scrollPosition.y
whenever _lastContentHeight > rect.height; change this so the auto-scroll only
happens when new history is added: track the number of entries (e.g.,
currentCount) and compare to a stored _lastHistoryCount, and only set
_scrollPosition.y = _lastContentHeight - rect.height when currentCount !=
_lastHistoryCount (then update _lastHistoryCount); otherwise leave
_scrollPosition untouched so the user’s manual scroll position is preserved.
Source/Core/Flywheel/EmbeddingSnapshotStore.cs-30-46 (1)

30-46: ⚠️ Potential issue | 🟠 Major

Record 在持锁状态下执行磁盘 I/O。

当缓冲达到 FlushThreshold 时,Record 会在持有 _lock 的情况下进入 FlushInternal 进行目录创建与文件追加;此时其他线程的 Record/Flush 全部阻塞。建议在锁内仅完成数据快照与缓冲清理,将 I/O 移到锁外执行,或通过单写线程消费队列。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Flywheel/EmbeddingSnapshotStore.cs` around lines 30 - 46, Record
currently calls FlushInternal while holding _lock, causing disk I/O under the
lock and blocking other threads; change Record and Flush to only capture a
snapshot of _buffer under _lock (e.g., create a local
List<EmbeddingSnapshotRecord> snapshot = new(...); snapshot.AddRange(_buffer);
_buffer.Clear()), release the lock, then call a new FlushInternal overload that
accepts the snapshot (e.g., FlushInternal(IEnumerable<EmbeddingSnapshotRecord>)
or similar) to perform directory creation and file append outside the lock;
likewise modify Flush to snapshot/clear under _lock and invoke the IO-performing
FlushInternal outside the lock to avoid long-held locks on _lock.
Source/Npc/NpcProfileBuilder.cs-31-83 (1)

31-83: ⚠️ Potential issue | 🟠 Major

Storyteller / Map NPC 的描述与 SystemPrompt、命令描述未本地化。

BuildPawnNpc 全部走翻译键(RimMind.Core.Prompt.*),但 BuildStorytellerNpc 与 BuildMapNpc 直接硬编码英文(CharacterDescription、SystemPrompt、以及 Commands 的 Description),中文等其它语言版本的玩家会看到混合语言。建议把这些字符串迁移到 Languages/*/Keyed/RimMind_Core.xml 并用 .Translate(...) 取出,与 pawn 路径保持一致。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Npc/NpcProfileBuilder.cs` around lines 31 - 83, The Storyteller and
Map NPC builders hardcode English strings; move all user-facing text
(CharacterDescription, SystemPrompt, Command Descriptions like in the Commands
list, ShortName/Name if needed) into language keys in
Languages/*/Keyed/RimMind_Core.xml and replace the literals in
BuildStorytellerNpc and BuildMapNpc with calls to .Translate("Your_Key_Here")
following the same pattern used by BuildPawnNpc (use the same key naming
convention and include parameters if needed); ensure each NpcCommand.Description
and any other string uses the corresponding translation key so all languages
display consistently.
Source/Core/Flywheel/EmbeddingSnapshotStore.cs-73-76 (1)

73-76: ⚠️ Potential issue | 🟠 Major

后台线程不应直接调用 Verse.Log。

Record 可能由后台线程触发刷写,命中 catch 时会从背景线程调用 Log.Warning,违反了"禁止从后台线程调用 RimWorld/Unity API"的约束。请改用 AIRequestQueue.LogFromBackground。

As per coding guidelines: "Use AIRequestQueue.LogFromBackground() for logging from background threads instead of direct Log calls".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Flywheel/EmbeddingSnapshotStore.cs` around lines 73 - 76, In the
catch block inside EmbeddingSnapshotStore (the code that flushes/records
embeddings), stop calling Verse.Log directly from the background thread and
replace Log.Warning($"[RimMind] EmbeddingSnapshot flush failed: {ex.Message}")
with a call to AIRequestQueue.LogFromBackground(...) passing a descriptive
message and the full exception details (ex.ToString()) so background-thread
logging uses the approved helper; also add any necessary using/import for
AIRequestQueue if missing.
Source/UI/Window_AgentDialogue.cs-156-181 (1)

156-181: ⚠️ Potential issue | 🟠 Major

后台线程的 catch 中直接调用 Log.Warning。

Task.Run(...) 中的 catch 运行在线程池线程,按本仓库约束应使用 AIRequestQueue.LogFromBackground 而非直接调用 Verse.Log。另外 ReplaceLastAssistantTurn 仅替换"最后一条 assistant",若主线程回调到来前用户又发送了新消息(再次写入 thinking 占位),可能替换到错的轮次;建议通过显式索引或基于 thinkingText 匹配并按索引替换。

As per coding guidelines: "Use AIRequestQueue.LogFromBackground() for logging from background threads instead of direct Log calls".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/UI/Window_AgentDialogue.cs` around lines 156 - 181, The background
Task.Run catch currently calls Verse.Log directly and uses
ReplaceLastAssistantTurn which can target the wrong turn; change the catch to
call AIRequestQueue.LogFromBackground(...) with a descriptive message and
exception details instead of Log.Warning, and in the
LongEventHandler.ExecuteWhenFinished callback replace the fragile
ReplaceLastAssistantTurn(npcId, ...) logic with an index-based replacement:
locate the exact history entry by matching role == "assistant" and content ==
thinkingText, record its index (from HistoryManager.Instance.GetHistory(npcId,
MaxHistoryRounds)), then call a targeted replace method (or add/extend
HistoryManager with ReplaceAssistantTurnAt(npcId, index, newContent)) to update
that specific entry with result.Message ?? "" so concurrent "thinking"
placeholders don’t cause wrong-turn replacement.
Source/Core/AgentBus/AgentBus.cs-10-10 (1)

10-10: ⚠️ Potential issue | 🟠 Major

_mainThreadId 通过静态字段初始化捕获,存在错绑风险。

静态字段在该类型被首次访问的线程上初始化,而不是必然在 RimWorld 主线程上。如果 AgentBus 被某条后台路径(例如某个 Task.Run 中的事件订阅)首次触碰,_mainThreadId 就会记成那个线程的 ID,之后所有 Publish 都会误判主线程并降级为 PublishFromBackground。建议加一个显式的 Initialize() 在 Mod 主入口(如 RimMindCoreMod 构造、MainTabWindow 启动等已知主线程时机)调用一次。

🛡️ 建议修复
-        private static readonly int _mainThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
+        private static int _mainThreadId = -1;
+
+        /// <summary>必须在主线程显式调用一次。</summary>
+        public static void InitializeOnMainThread()
+        {
+            _mainThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
+        }
@@
-            if (System.Threading.Thread.CurrentThread.ManagedThreadId != _mainThreadId)
+            if (_mainThreadId != -1 && System.Threading.Thread.CurrentThread.ManagedThreadId != _mainThreadId)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/AgentBus/AgentBus.cs` at line 10, The static initialization of
_mainThreadId in AgentBus is unsafe because it captures the thread ID at
type-first-access; change to lazy/uninitialized behavior and add a public
Initialize() method on AgentBus that sets _mainThreadId =
Thread.CurrentThread.ManagedThreadId and a bool _initialized flag, use
Initialize() from a known main-thread entrypoint (e.g., call
AgentBus.Initialize() in RimMindCoreMod constructor or MainTabWindow startup),
and ensure Publish/PublishFromBackground logic checks the _initialized flag (and
if not initialized, treat as main-thread or log and call Initialize) so the
correct main thread ID is used.
Source/Core/AgentBus/AgentBus.cs-62-75 (1)

62-75: ⚠️ Potential issue | 🟠 Major

_pendingDispatch 是共享静态 List,但 FlushBackgroundQueue 未加同步。

如果出现两条调用路径同时驱动 flush(比如玩家在 Mod 设置或调试面板手动 flush,恰逢 GameComponentTick 也在 flush),_pendingDispatch 会被并发 Clear/Add/索引读取,破坏内部状态。建议要么在方法体加 lock,要么改用每次调用本地分配的 List<AgentBusEvent>,并在文档中明确"仅主线程调用"。

♻️ 建议改为本地缓冲
-        private static readonly List<AgentBusEvent> _pendingDispatch
-            = new List<AgentBusEvent>();
@@
         public static void FlushBackgroundQueue()
         {
-            _pendingDispatch.Clear();
+            var pending = new List<AgentBusEvent>();
             while (_backgroundQueue.TryDequeue(out var evt))
-                _pendingDispatch.Add(evt);
+                pending.Add(evt);

-            for (int i = 0; i < _pendingDispatch.Count; i++)
+            for (int i = 0; i < pending.Count; i++)
             {
-                _pendingDispatch[i].Timestamp = Verse.Find.TickManager?.TicksGame ?? 0;
-                DispatchToHandlers(_pendingDispatch[i]);
+                pending[i].Timestamp = Verse.Find.TickManager?.TicksGame ?? 0;
+                DispatchToHandlers(pending[i]);
             }
-
-            _pendingDispatch.Clear();
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/AgentBus/AgentBus.cs` around lines 62 - 75, FlushBackgroundQueue
mutates the shared static List _pendingDispatch without synchronization causing
race conditions; fix by making FlushBackgroundQueue allocate a local
List<AgentBusEvent> (e.g., var local = new List<AgentBusEvent>()) then drain
_backgroundQueue into that local list and operate on local (set Timestamp and
call DispatchToHandlers) instead of touching _pendingDispatch, or alternatively
protect the existing _pendingDispatch usage with a lock around
Clear/Add/iteration; update comments to state that _backgroundQueue is still
thread-safe but FlushBackgroundQueue should be main-thread-only if you keep the
shared list.
Source/Core/Flywheel/FlywheelRuleEngine.cs-159-176 (1)

159-176: ⚠️ Potential issue | 🟠 Major

遥测样本无有效数据时返回 0f 会触发误报建议。

ComputeAvgBudgetUtilization 在所有记录都不满足 BudgetValue>0 && TotalTokens>0 时返回 0f,随后命中 Line 40 < 0.5f 阈值,产生「Budget utilization 0%,建议下调 TotalBudget」的错误推荐。ComputeAvgCacheHitRate(Line 178-194)同样会因缺数据返回 0f,触发 Line 70 < 0.3f 的 Alpha 下调推荐。建议在样本不足时返回标识缺数据的值(如 float.NaN),并在规则判断处跳过。

建议修复
-            return count > 0 ? sum / count : 0f;
+            return count > 0 ? sum / count : float.NaN;

并在 Line 40/70/85/100 调用处增加 !float.IsNaN(...) 守卫,例如:

-            if (avgBudgetUtil < 0.5f && store != null)
+            if (!float.IsNaN(avgBudgetUtil) && avgBudgetUtil < 0.5f && store != null)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Flywheel/FlywheelRuleEngine.cs` around lines 159 - 176,
ComputeAvgBudgetUtilization currently returns 0f when no valid telemetry records
exist which causes false positives; change ComputeAvgBudgetUtilization (and
similarly ComputeAvgCacheHitRate) to return float.NaN when count == 0 to
indicate insufficient data, and update all rule checks that compare those
results against thresholds (the places that currently do comparisons like result
< 0.5f or result < 0.3f) to first guard with !float.IsNaN(value) before
performing the threshold comparison so rules are skipped when telemetry is
missing.
Source/Client/OpenAI/OpenAIClient.cs-29-34 (1)

29-34: ⚠️ Potential issue | 🟠 Major

_formatCapabilityCache 为非线程安全 Dictionary,多并发请求下可能数据竞争。

maxConcurrentRequests 默认为 3,多个 SendAsync/SendStructuredAsync 在背景线程同时读写该静态字典(Line 65、82、89、164、188、190、203),可能触发 InvalidOperationException 或读取脏数据。结合本仓库另一处 learning「Implement AgentBus event handlers with thread-safe subscription using ConcurrentDictionary」,此处也应改用 ConcurrentDictionary<string, string>。

建议修复
-        private static readonly Dictionary<string, string> _formatCapabilityCache = new();
+        private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, string> _formatCapabilityCache = new();

随后 Line 82/89/188/190/203 的赋值需改为 _formatCapabilityCache[cacheKey] = mode(索引器在 ConcurrentDictionary 上是线程安全的)或改用 AddOrUpdate。

Based on learnings: Implement AgentBus event handlers with thread-safe subscription using ConcurrentDictionary with independent locks per handler list.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Client/OpenAI/OpenAIClient.cs` around lines 29 - 34, The static field
_formatCapabilityCache is not thread-safe; replace Dictionary<string,string>
with ConcurrentDictionary<string,string> and keep InvalidateFormatCache to call
Clear(); then update all concurrent access sites (e.g., reads/writes from
SendAsync and SendStructuredAsync and any other spots that set cache entries) to
use the ConcurrentDictionary indexer (e.g., _formatCapabilityCache[cacheKey] =
mode) or ConcurrentDictionary.AddOrUpdate to perform thread-safe updates; ensure
any reads use TryGetValue where appropriate to avoid races.
Source/Core/Context/EmbedCache.cs-121-157 (1)

121-157: ⚠️ Potential issue | 🟠 Major

EvictBlockIfNeeded 在驱逐 NPC 时未清理 _blockLayer,存在内存泄漏。

驱逐 victim 时只移除了 _blockOrder 与 _blockCache,但 _blockLayer 中以 npcId + ":" + key 为键的条目仍保留。长期运行后这些条目会持续累积;同时也会让后续 EvictBlockIfNeeded 中的 hasProtected 判断对未来同名 NPC 的新数据被错误地视为受保护项。建议参考 InvalidateNpc 的清理逻辑同步移除。

🛠️ 建议修复
                 if (victim == null) break;
                 _blockOrder.Remove(victim);
                 if (_blockCache.TryGetValue(victim, out var victimDict))
                 {
                     _blockCount -= victimDict.Count;
                     _blockCache.Remove(victim);
                 }
+                // 同步清理 _blockLayer,避免悬挂条目长期累积
+                var prefix = victim + ":";
+                var layerKeysToRemove = new List<string>();
+                foreach (var lk in _blockLayer.Keys)
+                {
+                    if (lk.StartsWith(prefix))
+                        layerKeysToRemove.Add(lk);
+                }
+                foreach (var lk in layerKeysToRemove)
+                    _blockLayer.Remove(lk);
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Context/EmbedCache.cs` around lines 121 - 157, EvictBlockIfNeeded
currently removes victim from _blockOrder and _blockCache but leaves per-entry
metadata in _blockLayer, causing leaks; update the eviction path (inside the
victim != null branch, where victimDict is obtained) to iterate the victim's
keys and remove corresponding _blockLayer entries whose keys are formed as
victim + ":" + key (same pattern used in InvalidateNpc), ensuring you remove the
same set of layer entries when you decrement _blockCount and remove victim from
_blockCache/_blockOrder.
Source/Core/Flywheel/FlywheelTelemetryCollector.cs-99-102 (1)

99-102: ⚠️ Potential issue | 🟠 Major

后台线程不应直接调用 Log.Warning。

如果 Flush/Record 可能被后台线程触发(例如非主线程的请求处理路径),按编码规范应改用 AIRequestQueue.LogFromBackground,避免从后台线程直接调用 RimWorld API。

As per coding guidelines: "Use AIRequestQueue.LogFromBackground() for logging from background threads instead of direct Log calls".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Flywheel/FlywheelTelemetryCollector.cs` around lines 99 - 102,
The catch block in FlywheelTelemetryCollector (where Flush/Record calls are
wrapped) currently calls Log.Warning directly, which is invalid from background
threads; replace the direct Log.Warning call with
AIRequestQueue.LogFromBackground wrapping the log action (e.g.,
AIRequestQueue.LogFromBackground(() => Log.Warning(...))) so background-thread
logging uses the prescribed helper; update the catch in
FlywheelTelemetryCollector.Flush/Record to call AIRequestQueue.LogFromBackground
with the same message including ex.Message to preserve details.
Source/Core/Flywheel/FlywheelTelemetryCollector.cs-43-103 (1)

43-103: ⚠️ Potential issue | 🟠 Major

文件 I/O 在锁内执行会阻塞所有遥测记录路径。

Record() 在累积到阈值后会同步触发 FlushInternal,整个序列化与 File.AppendAllLines 都在 _lock 内完成。慢盘或大批量写入会让所有 Record/Flush/GetRecentRecords 调用在锁上排队,影响主路径与其它后台线程。建议:在锁内仅做 swap(拷贝当前 _buffer 并清空),把序列化与文件写入挪到锁外执行;或把持久化扔到独立的后台线程/队列。

🛠️ 建议重构思路
-        public void Record(TelemetryRecord record)
-        {
-            lock (_lock)
-            {
-                _buffer.Add(record);
-                _recentRecords.Add(record);
-                if (_recentRecords.Count > RecentRecordsCapacity)
-                    _recentRecords.RemoveRange(0, _recentRecords.Count - RecentRecordsCapacity);
-                if (_buffer.Count >= FlushThreshold)
-                    FlushInternal();
-            }
-        }
+        public void Record(TelemetryRecord record)
+        {
+            List<TelemetryRecord>? toFlush = null;
+            lock (_lock)
+            {
+                _buffer.Add(record);
+                _recentRecords.Add(record);
+                if (_recentRecords.Count > RecentRecordsCapacity)
+                    _recentRecords.RemoveRange(0, _recentRecords.Count - RecentRecordsCapacity);
+                if (_buffer.Count >= FlushThreshold)
+                {
+                    toFlush = new List<TelemetryRecord>(_buffer);
+                    _buffer.Clear();
+                }
+            }
+            if (toFlush != null) WriteToDisk(toFlush);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Flywheel/FlywheelTelemetryCollector.cs` around lines 43 - 103,
Record/Flush currently perform JSON serialization and File.AppendAllLines while
holding _lock, blocking all Record/GetRecentRecords/Flush callers; fix by making
FlushInternal (invoked by Record and Flush) only swap out the _buffer under
_lock (copy to local toWrite and clear _buffer) and then release the lock and
perform serialization and file I/O outside the lock; keep recent records updates
( _recentRecords manipulation ) under the same lock in Record, and ensure
Flush() still calls the new FlushInternal which quickly swaps the buffer and
returns while heavy work runs after lock release (or consider delegating the
outside-the-lock write to a background task), referencing the methods/fields
Record, FlushInternal, Flush, _buffer, _lock and _recentRecords.
Source/Core/Context/ContextEngine.cs-250-279 (1)

250-279: ⚠️ Potential issue | 🟠 Major

当 vector 为 null 时不应写入嵌入快照。

第 262–264 行尝试从 SemanticEmbedding 取值并回退到 key.KeyEmbedding,但两者都可能为 null。第 273 行 Vector = vector! 只是抑制了编译器警告,实际会把 null(或空)向量写入 EmbeddingSnapshotStore,污染后续相似度查询/分析报告。建议在 vector == null 时跳过本次 Record。

🛠️ 建议修复
                     float[]? vector = SemanticEmbedding.GetBlockEmbedding(request.NpcId, key.Key);
                     if (vector == null && key.KeyEmbedding != null)
                         vector = key.KeyEmbedding;
+                    if (vector == null) continue;
                     float relevanceScore = snapshot.KeyScores.TryGetValue(key.Key, out var score) ? score : 0f;
                     _embeddingSnapshotStore.Record(new EmbeddingSnapshotRecord
                     {
                         ...
-                        Vector = vector!,
+                        Vector = vector,
                         ...
                     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Context/ContextEngine.cs` around lines 250 - 279, The code
currently forces a possibly-null vector into EmbeddingSnapshotRecord via Vector
= vector! which can write null embeddings; update the loop in ContextEngine.cs
so that after retrieving vector via
SemanticEmbedding.GetBlockEmbedding(request.NpcId, key.Key) and falling back to
key.KeyEmbedding you check if vector == null and, if so, skip calling
_embeddingSnapshotStore.Record entirely for that key; remove the use of the
null-forgiving operator on vector and only create/record an
EmbeddingSnapshotRecord when vector is non-null (refer to
SemanticEmbedding.GetBlockEmbedding, key.KeyEmbedding, EmbeddingSnapshotRecord,
and _embeddingSnapshotStore.Record).
Source/Npc/Player2StorageDriver.cs-35-71 (1)

35-71: ⚠️ Potential issue | 🟠 Major

多处 catch { return ... } 静默吞错,与 PR 摘要中"replace empty catches with warnings"不一致

SpawnNpcAsync (60)、KillNpcAsync (70)、GetHistoryAsync (194)、PutAsync (206)、GetAsync (216)、DeleteAsync (226)、GetBatchAsync (240) 都直接 catch { return ...; }。一旦远程接口返回非预期结构、序列化异常、网络层抛错,调用方完全看不到原因,调试和告警链路都断了。

请对每处 catch 至少改成捕获 Exception ex 并通过 AIRequestQueue.LogFromBackground($"[RimMind] Player2StorageDriver.<API> failed: {ex.Message}", isWarning: true); 上报。注意 SpawnNpcAsync / KillNpcAsync 这种入参可能含 npcId,记录时也带上。

🩹 示例修复(其他方法同理)
-            catch { return false; }
+            catch (Exception ex)
+            {
+                AIRequestQueue.LogFromBackground(
+                    $"[RimMind] Player2StorageDriver.SpawnNpcAsync failed for '{profile?.NpcId}': {ex.Message}",
+                    isWarning: true);
+                return false;
+            }

As per coding guidelines: "All background thread logging must use AIRequestQueue.LogFromBackground() instead of Log.* directly".

Also applies to: 187-241

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Npc/Player2StorageDriver.cs` around lines 35 - 71, Multiple methods
(SpawnNpcAsync, KillNpcAsync, GetHistoryAsync, PutAsync, GetAsync, DeleteAsync,
GetBatchAsync) currently swallow exceptions with bare catch blocks; replace each
bare catch with catch (Exception ex) and call
AIRequestQueue.LogFromBackground($"[RimMind] Player2StorageDriver.<MethodName>
failed: {ex.Message}{optional context}", isWarning: true) before returning the
existing fallback value—include contextual data like npcId for
SpawnNpcAsync/KillNpcAsync in the log string; ensure every background-thread
catch uses AIRequestQueue.LogFromBackground rather than any other logger and
preserves the original return behavior.
Source/Npc/Player2StorageDriver.cs-73-76 (1)

73-76: ⚠️ Potential issue | 🟠 Major

IsNpcAlive 名字暗示查 Player2,但实现只查本地 NpcManager

Player2StorageDriver.IsNpcAlive 直接走 NpcManager.Instance?.IsNpcAlive(npcId),根本没访问 Player2。问题会沿调用链放大:HybridStorageDriver.IsNpcAlive 会做 _local.IsNpcAlive(npcId) || _remote.IsNpcAlive(npcId),两边其实都是同一个 NpcManager 维度的查询,"远程兜底"语义被破坏,且远端真正存在但本地不存在的 NPC 永远会被判定为不在。

请按职责实现:要么真正调一次 _client.GetRawAsync($"/npcs/{npcId}/status") 并解析(注意这是同步接口需要权衡阻塞),要么把这个方法标注为"代理本地状态"并在文档中说明,避免上层依赖错位。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Npc/Player2StorageDriver.cs` around lines 73 - 76,
Player2StorageDriver.IsNpcAlive currently only queries NpcManager.Instance
(local), breaking the intended local-then-remote semantics used by
HybridStorageDriver.IsNpcAlive; fix by either (A) implementing a real remote
status check: perform a synchronous/awaited call to
_client.GetRawAsync($"/npcs/{npcId}/status") inside
Player2StorageDriver.IsNpcAlive, parse the response to determine alive state and
handle timeouts/errors, or (B) explicitly mark Player2StorageDriver.IsNpcAlive
as "local-only" (update docs/comments and method XML summary) so callers know it
does not consult Player2, and update HybridStorageDriver usage accordingly;
reference Player2StorageDriver.IsNpcAlive, HybridStorageDriver.IsNpcAlive,
NpcManager.Instance and _client.GetRawAsync when making the change.
Source/Core/Context/BudgetScheduler.cs-36-71 (1)

36-71: ⚠️ Potential issue | 🟠 Major

W1/W2 归一化逻辑不一致

OnParameterChanged 在收到 w1 或 w2 变更时会强制 W1 + W2 = 1,但 ApplyStoreParameters 直接独立读 store.Get("w1") 和 store.Get("w2") 而不做归一化。这会导致:

  1. 初始/订阅时若 store 中 w1+w2 ≠ 1,调度器会以非归一化权重运行;而 Schedule 中的阈值 threshold = 1.0f - B(第 97 行)和评分 w1*P + w2*E(第 106 行)的语义都假设 w1+w2=1,否则筛选行为会偏离预期。
  2. 触发任一参数变更后又自动归一化,行为前后不一致。

建议统一策略:要么始终把 w1/w2 视为一对耦合参数(在 ApplyStoreParameters 也强制归一化),要么删除 OnParameterChanged 里的耦合逻辑、由 store 保证一致性。

♻️ 建议修复(在 Apply 时归一化)
         private void ApplyStoreParameters(FlywheelParameterStore store)
         {
-            _config.W1 = store.Get("w1");
-            _config.W2 = store.Get("w2");
+            float w1 = store.Get("w1");
+            float w2 = store.Get("w2");
+            float sum = w1 + w2;
+            if (sum > 1e-6f) { _config.W1 = w1 / sum; _config.W2 = w2 / sum; }
+            else { _config.W1 = 0.5f; _config.W2 = 0.5f; }
             _config.Alpha = store.Get("Alpha");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Context/BudgetScheduler.cs` around lines 36 - 71,
OnParameterChanged normalizes W1/W2 but ApplyStoreParameters does not, causing
inconsistent behavior; update ApplyStoreParameters (which reads store.Get("w1")
and store.Get("w2")) to normalize the pair so _config.W1 + _config.W2 == 1 after
assignment (e.g., compute sum = w1+w2, if sum > 0 divide each by sum, else fall
back to a safe default like W1=1,W2=0), and assign the normalized values to
_config.W1 and _config.W2 so behavior matches OnParameterChanged.
Source/Client/Player2/Player2Client.cs-308-328 (1)

308-328: ⚠️ Potential issue | 🟠 Major

心跳循环遇到一次异常就永久关闭

StartHealthCheckLoopAsync 一旦进入 catch 就把 _healthCheckActive = false,这意味着任何瞬时网络抖动或 Task.Delay 异常都会让心跳永久停止;后续无法恢复,因为构造函数中的 if (!_healthCheckActive ...) 也只在第一次创建时启动。建议把异常局部捕获在 while 循环内部,记录 warning 后继续下一轮,仅在 Current.Game == null 等明确退出条件下才停止循环。

♻️ 建议修复
         private async Task StartHealthCheckLoopAsync()
         {
-            try
-            {
-                while (_healthCheckActive && Current.Game != null)
-                {
-                    await Task.Delay(60000);
-                    if (_healthCheckActive) await EnsureHealthCheck(force: true);
-                }
-            }
-            catch (Exception ex)
-            {
-                AIRequestQueue.LogFromBackground($"[RimMind] Player2 health check loop crashed: {ex.Message}", isWarning: true);
-                _healthCheckActive = false;
-            }
+            while (_healthCheckActive && Current.Game != null)
+            {
+                try
+                {
+                    await Task.Delay(60000);
+                    if (_healthCheckActive && Current.Game != null)
+                        await EnsureHealthCheck(force: true);
+                }
+                catch (Exception ex)
+                {
+                    AIRequestQueue.LogFromBackground(
+                        $"[RimMind] Player2 health check iteration failed: {ex.Message}", isWarning: true);
+                }
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Client/Player2/Player2Client.cs` around lines 308 - 328,
StartHealthCheckLoopAsync currently sets _healthCheckActive = false in a single
outer catch which permanently stops the loop on any exception; instead, move the
try/catch inside the while so each iteration catches exceptions from
EnsureHealthCheck, Task.Delay, etc., logs via AIRequestQueue.LogFromBackground
(include ex.Message), and continues to the next iteration; only break/clear
_healthCheckActive when explicit shutdown conditions occur (e.g., Current.Game
== null or an intentional stop), leaving StartHealthCheckLoop unchanged to kick
off the async loop.
Source/Core/Context/SemanticEmbedding.cs-205-208 (1)

205-208: ⚠️ Potential issue | 🟠 Major

Math.Clamp 在 .NET Framework 4.8 上不可用,导致编译失败

System.Math.Clamp 仅在 .NET Core 2.0+ 和 .NET Standard 2.1 中提供,.NET Framework 4.8 及更早版本均无此方法。本项目目标框架为 net48(见 RimMindCore.csproj),因此这两处调用都会编译失败:

  • Source/Core/Context/SemanticEmbedding.cs:207
  • Source/Core/Context/BudgetScheduler.cs:89

改用 UnityEngine.Mathf.Clamp 替代(参考同项目 StrategyOptimizer.cs 的用法):

修复方式

SemanticEmbedding.cs 第 207 行:

-            return Math.Clamp(dot / denom, 0f, 1f);
+            return UnityEngine.Mathf.Clamp(dot / denom, 0f, 1f);

BudgetScheduler.cs 第 89 行:

-            float B = Math.Clamp(budget, 0f, 1f);
+            float B = UnityEngine.Mathf.Clamp(budget, 0f, 1f);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Context/SemanticEmbedding.cs` around lines 205 - 208, The project
targets net48 where System.Math.Clamp is unavailable; replace calls to
Math.Clamp with UnityEngine.Mathf.Clamp (e.g., the clamp in the cosine
similarity method in SemanticEmbedding.cs and the clamp usage in
BudgetScheduler.cs) and ensure the files include a using UnityEngine; directive
if not already present; keep the same float arguments (0f, 1f) so signatures and
behavior remain identical.
Source/AICoreAPI.cs-742-763 (1)

742-763: 🛠️ Refactor suggestion | 🟠 Major

新加的几组 List 注册表缺少线程安全,且没有解注册时清理 modId。

  • _parameterTuners / _sensorProviders / _agentModeProviders / _streamingHandlers 都是普通 List<T>,而本 PR 同时引入了"主线程外的 AgentBus / Perception 事件分发"。如果有任何一处 sub-mod 在背景线程上执行 Register* 或迭代上面这些列表(例如 IsPawnAgentControlled 在背景判定),List<T> 会抛 InvalidOperationException。建议改成 lock 包裹的访问或者 ImmutableList/CopyOnWrite。
  • Unregister* 仅按 Id 字符串删除,缺少基于 modId 的批量解注册;UnregisterModProviders 目前只清理三类 context provider,无法连带回收上述 4 组 handler,sub-mod 卸载时会留下悬挂引用。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/AICoreAPI.cs` around lines 742 - 763, The new registries
(_parameterTuners, _sensorProviders, _agentModeProviders, _streamingHandlers)
are not thread-safe and lack per-mod cleanup; change their implementation to a
thread-safe pattern (e.g., use locks around all mutations/iterations or switch
to immutable/copy-on-write collections) and update
RegisterParameterTuner/RegisterSensorProvider/RegisterAgentModeProvider/RegisterStreamingHandler
and all readers (including IsPawnAgentControlled) to use that pattern to prevent
InvalidOperationException on concurrent access; additionally extend
UnregisterModProviders to remove entries from these four lists by matching a
modId property (or expose/unify an owner/modId on
IParameterTuner/ISensorProvider/IAgentModeProvider/IStreamingResponseHandler) so
unloading a mod cleans up its registered items, and keep existing Unregister* by
Id behavior intact.
Source/AICoreAPI.cs-258-288 (1)

258-288: ⚠️ Potential issue | 🟠 Major

两个空 catch 与降级链路丢失结构化契约。

  1. Line 259 与 Line 267 出现 catch { },完全吞掉异常。这与本 PR 的目标"replace empty catches with warnings"直接冲突;遥测异常被静默后会让 Flywheel 数据缺口非常难定位。
  2. Line 269-280 的降级 fallbackRequest 丢弃了 schema 与 tools,然后通过 queue.Enqueue 走普通文本路径。调用方拿到的 AIResponse 仍带 UseJsonMode = true 但实际上模型并未收到 schema/tool 定义,后续 JSON 解析很可能失败,且异常已被吞,排查时几乎没有线索。

建议:把空 catch 至少改为 Log.Warning(主线程)或 AIRequestQueue.LogFromBackground(回调可能在后台);降级路径要么直接 Failure(...) 让上层感知,要么找到一种能继续传递 schema 的入队方式。

🛠️ 最小修复示例
-                catch { }
+                catch (Exception ex)
+                {
+                    AIRequestQueue.LogFromBackground(
+                        $"[RimMind] Telemetry record failed: {ex.Message}", isWarning: true);
+                }
                 onComplete?.Invoke(response);
             };

             try
             {
                 RequestStructuredAsync(aiRequest, schema, wrappedOnComplete, tools);
             }
-            catch
+            catch (Exception ex)
             {
-                var fallbackRequest = new AIRequest
-                {
-                    ...
-                };
-                var queue = AIRequestQueue.Instance;
-                var client = GetClient();
-                if (queue != null && client != null)
-                    queue.Enqueue(fallbackRequest, wrappedOnComplete, client);
-                else
-                    wrappedOnComplete?.Invoke(AIResponse.Failure(fallbackRequest.RequestId, "No AI client available"));
+                Log.Warning($"[RimMind] RequestStructured dispatch failed: {ex.Message}");
+                wrappedOnComplete?.Invoke(AIResponse.Failure(aiRequest.RequestId,
+                    $"Structured dispatch failed: {ex.Message}"));
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/AICoreAPI.cs` around lines 258 - 288, The two empty catch blocks
around the RequestStructuredAsync call swallow exceptions and drop telemetry;
replace them with explicit logging (use Log.Warning on main-thread or
AIRequestQueue.LogFromBackground if called from a background callback) and
include the caught exception details and context (aiRequest.RequestId, ModId).
Also fix the fallback path that builds fallbackRequest: either enqueue a true
structured fallback that preserves schema and tools (propagate schema and tools
into the fallback AIRequest and ensure queue.Enqueue receives them), or
immediately return a failure via onComplete/wrappedOnComplete (e.g.,
AIResponse.Failure) so callers know structured execution failed; reference
RequestStructuredAsync, wrappedOnComplete, AIRequest (fallbackRequest),
AIRequestQueue.Instance/Enqueue, GetClient(), and AIResponse.Failure when
applying the change.
Source/AICoreAPI.cs-207-218 (1)

207-218: ⚠️ Potential issue | 🟠 Major

SystemPrompt = null! 违反类型契约,应使用 string.Empty 占位。

AIRequest.SystemPrompt 定义为非可空 string(Source/Client/AIRequest.cs 第 7 行),但在 AICoreAPI.cs 第 209 和 271 行以及 PawnAgent.cs 第 113 行被赋值 null!。

虽然 OpenAIClient 和 Player2Client 在读取时有防御性的 IsNullOrEmpty 检查,但使用 null! 强行赋空值仍然是不良实践——它违反了属性的非可空类型声明,容易在后续重构中引入缺陷。建议统一改为 string.Empty 以保持类型安全和一致性。

🛠️ 建议修改
-                SystemPrompt = null!,
+                SystemPrompt = string.Empty,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/AICoreAPI.cs` around lines 207 - 218, The AIRequest.SystemPrompt
property is non-nullable but is being assigned null! in several places; replace
those null! assignments with string.Empty to respect the type contract and avoid
hidden nullability violations—specifically update the AIRequest initializer in
AICoreAPI where SystemPrompt = null! (and the other occurrence in AICoreAPI
around the later initializer) and the assignment in PawnAgent (where
SystemPrompt is set to null!), ensuring all uses of AIRequest.SystemPrompt use
string.Empty as the placeholder.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a83de4a3-2001-4a73-b7a5-d6b071dbb25e

📥 Commits

Reviewing files that changed from the base of the PR and between d1b2d95 and 7034299.

⛔ Files ignored due to path filters (6)
  • .github/scripts/analyze_release.py is excluded by !.github/**
  • .github/workflows/pr-review.yml is excluded by !.github/**
  • .github/workflows/release.yml is excluded by !.github/**
  • AGENTS.md is excluded by !**/*.md
  • README.md is excluded by !**/*.md
  • script/deploy-single.sh is excluded by !script/**
📒 Files selected for processing (112)
  • .coderabbit.yaml
  • About/About.xml
  • About/PublishedFileId.txt
  • Defs/MainButtons/AICore_DebugLog.xml
  • INTRODUCTION.txt
  • Languages/ChineseSimplified/Keyed/RimMind_Core.xml
  • Languages/English/Keyed/RimMind_Core.xml
  • Source/AICoreAPI.cs
  • Source/Client/AIRequest.cs
  • Source/Client/AIResponse.cs
  • Source/Client/IAIClient.cs
  • Source/Client/JsonRepairHelper.cs
  • Source/Client/OpenAI/OpenAIClient.cs
  • Source/Client/OpenAI/OpenAIDto.cs
  • Source/Client/Player2/Player2Client.cs
  • Source/Client/Player2/Player2Models.cs
  • Source/Client/QuotaExceededException.cs
  • Source/Client/StructuredTool.cs
  • Source/Comps/CompPawnAgent.cs
  • Source/Core/AIDebugLog.cs
  • Source/Core/AIRequestQueue.cs
  • Source/Core/Agent/AgentGoal.cs
  • Source/Core/Agent/AgentGoalStack.cs
  • Source/Core/Agent/AgentIdentity.cs
  • Source/Core/Agent/AgentState.cs
  • Source/Core/Agent/AgentStateTransition.cs
  • Source/Core/Agent/BehaviorRecord.cs
  • Source/Core/Agent/CooldownFilter.cs
  • Source/Core/Agent/DedupFilter.cs
  • Source/Core/Agent/DefaultAgentProvider.cs
  • Source/Core/Agent/GoalGenerator.cs
  • Source/Core/Agent/IAgentActionBridge.cs
  • Source/Core/Agent/IAgentProvider.cs
  • Source/Core/Agent/IPerceptionFilter.cs
  • Source/Core/Agent/PawnAgent.cs
  • Source/Core/Agent/PerceptionBuffer.cs
  • Source/Core/Agent/PerceptionPipeline.cs
  • Source/Core/Agent/PriorityFilter.cs
  • Source/Core/Agent/RiskLevel.cs
  • Source/Core/Agent/StrategyOptimizer.cs
  • Source/Core/AgentBus/AgentBus.cs
  • Source/Core/AgentBus/AgentBusEvent.cs
  • Source/Core/AgentBus/AgentBusEventType.cs
  • Source/Core/AgentBus/EventBusAdapter.cs
  • Source/Core/AgentBus/Events/ActionEvent.cs
  • Source/Core/AgentBus/Events/AgentLifecycleEvent.cs
  • Source/Core/AgentBus/Events/DecisionEvent.cs
  • Source/Core/AgentBus/Events/GoalEvent.cs
  • Source/Core/AgentBus/Events/MemoryEvent.cs
  • Source/Core/AgentBus/Events/PerceptionEvent.cs
  • Source/Core/AgentBus/IEventBus.cs
  • Source/Core/Context/BudgetScheduleResult.cs
  • Source/Core/Context/BudgetScheduler.cs
  • Source/Core/Context/BudgetSchedulerConfig.cs
  • Source/Core/Context/ContextDiff.cs
  • Source/Core/Context/ContextEngine.cs
  • Source/Core/Context/ContextEntry.cs
  • Source/Core/Context/ContextKeyRegistry.cs
  • Source/Core/Context/ContextLayer.cs
  • Source/Core/Context/ContextRequest.cs
  • Source/Core/Context/ContextSnapshot.cs
  • Source/Core/Context/EmbedCache.cs
  • Source/Core/Context/HistoryEntry.cs
  • Source/Core/Context/HistoryGameComponent.cs
  • Source/Core/Context/HistoryManager.cs
  • Source/Core/Context/KeyMeta.cs
  • Source/Core/Context/RelevanceTable.cs
  • Source/Core/Context/ScenarioRegistry.cs
  • Source/Core/Context/SchemaRegistry.cs
  • Source/Core/Context/SemanticEmbedding.cs
  • Source/Core/Extensions/IAgentModeProvider.cs
  • Source/Core/Extensions/IAudioPlayer.cs
  • Source/Core/Extensions/IParameterTuner.cs
  • Source/Core/Extensions/ISensorProvider.cs
  • Source/Core/Extensions/IStreamingResponseHandler.cs
  • Source/Core/Flywheel/EmbeddingSnapshotStore.cs
  • Source/Core/Flywheel/FlywheelAnalysisReport.cs
  • Source/Core/Flywheel/FlywheelAutoApplyMode.cs
  • Source/Core/Flywheel/FlywheelGameComponent.cs
  • Source/Core/Flywheel/FlywheelParameterStore.cs
  • Source/Core/Flywheel/FlywheelRuleEngine.cs
  • Source/Core/Flywheel/FlywheelTelemetryCollector.cs
  • Source/Core/GameContextBuilder.cs
  • Source/Core/Perception/PerceptionBridge.cs
  • Source/Core/Prompt/ContextComposer.cs
  • Source/Core/Prompt/PromptBudget.cs
  • Source/Core/Prompt/PromptSection.cs
  • Source/Core/Prompt/StructuredPromptBuilder.cs
  • Source/Core/Prompt/TaskInstructionBuilder.cs
  • Source/Debug/AICoreDebugActions.cs
  • Source/Npc/HybridStorageDriver.cs
  • Source/Npc/IStorageDriver.cs
  • Source/Npc/LocalStorageDriver.cs
  • Source/Npc/MapNpcComponent.cs
  • Source/Npc/NpcManager.cs
  • Source/Npc/NpcModels.cs
  • Source/Npc/NpcProfileBuilder.cs
  • Source/Npc/Patch_MapNpcLifecycle.cs
  • Source/Npc/Player2StorageDriver.cs
  • Source/Npc/ResponseDispatcher.cs
  • Source/Npc/StorageDriverFactory.cs
  • Source/Patch/AITogglePatch.cs
  • Source/Patch/PerceptionBridge_PatchDowned.cs
  • Source/Patch/PerceptionBridge_PatchHealth.cs
  • Source/Patch/PerceptionBridge_PatchMentalState.cs
  • Source/Patch/PerceptionBridge_PatchMood.cs
  • Source/Patch/PerceptionBridge_PatchRaid.cs
  • Source/Settings/AICoreSettings.cs
  • Source/Settings/ContextSettings.cs
  • Source/UI/AICoreSettingsUI.cs
  • Source/UI/Window_AIDebugLog.cs
  • Source/UI/Window_AgentDialogue.cs
💤 Files with no reviewable changes (1)
  • Source/Core/Prompt/StructuredPromptBuilder.cs

Comment thread Source/AICoreAPI.cs Outdated
Comment thread Source/Client/QuotaExceededException.cs Outdated
Comment thread Source/Core/AgentBus/AgentBus.cs Outdated
Comment thread Source/backup/Kernel/Context/BudgetScheduler.cs Outdated
Comment thread Source/Core/Flywheel/EmbeddingSnapshotStore.cs Outdated
Comment on lines +16 to +17
public FlywheelGameComponent() : base() { }
public FlywheelGameComponent(Game game) : base() { }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 验证 RimWorld GameComponent 在仓库中的实际基类签名以及现有 GameComponent 子类的构造模式
rg -nP --type=cs -C2 'class\s+\w+\s*:\s*GameComponent\b'
rg -nP --type=cs -C2 ':\s*base\s*\('

Repository: RimWorld-RimMind-Mod/RimWorld-RimMind-Mod-Core

Length of output: 9397


🌐 Web query:

RimWorld Verse GameComponent class constructor signature 1.6

💡 Result:

For RimWorld 1.6, a Verse GameComponent subclass must provide a parameterized constructor whose signature is: public YourGameComponent(Game game) : base(game) { } Key requirement: the constructor parameter type must be Verse.Game; otherwise RimWorld throws “Constructor not found” when instantiating the component. Example (from modding guidance for GameComponent): public class MyExampleGameComponent : GameComponent { public MyExampleGameComponent(Game game) : base(game) { } } If you want, share your component class name and base type (GameComponent vs a more specific subclass) and I can format the exact constructor you need.

Citations:


修复 GameComponent 构造器:必须传递 game 参数给基类。

RimWorld 1.6 要求 GameComponent 子类构造器必须按以下规范实现:

  1. 仅保留 (Game game) 签名的单一构造器(不需要无参构造器)
  2. 必须显式调用 : base(game) 将 game 参数传递给基类

当前代码两个构造器都调用 : base() 而未传递 game 参数,这会导致 RimWorld 1.6 自动实例化失败。

🛡️ 建议的修复
-        public FlywheelGameComponent() : base() { }
-        public FlywheelGameComponent(Game game) : base() { }
+        public FlywheelGameComponent(Game game) : base(game) { }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Flywheel/FlywheelGameComponent.cs` around lines 16 - 17, 当前
FlywheelGameComponent 的两个构造函数都调用了 base() 并且存在无参构造器,这会导致 RimWorld 1.6
自动实例化失败;修改为只保留带参构造器 FlywheelGameComponent(Game game) 且在定义中显式调用基类构造器为
base(game)(删除无参构造器和任何对 base() 的调用),确保唯一构造函数签名为 (Game game) 并将该参数传递给基类。

Comment thread Source/Core/Flywheel/FlywheelParameterStore.cs Outdated
Comment thread Source/Core/Flywheel/FlywheelTelemetryCollector.cs Outdated
Comment on lines +10 to +28
public static void Dispatch(NpcChatResult result, Pawn? pawn = null)
{
if (result == null) return;

if (!string.IsNullOrEmpty(result.Error))
{
Log.Warning($"[RimMind] ResponseDispatcher: chat error - {result.Error}");
return;
}

if (!string.IsNullOrEmpty(result.Message))
DispatchMessage(result.Message, pawn);

if (result.Commands != null && result.Commands.Count > 0)
DispatchCommands(result.Commands.ToArray(), pawn);

if (!string.IsNullOrEmpty(result.AudioUrl))
DispatchAudio(result.AudioUrl!);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 找出所有调用 ResponseDispatcher.Dispatch 的位置,确认所在线程上下文
rg -nP --type=cs -C5 '\bResponseDispatcher\.\w+\s*\('

Repository: RimWorld-RimMind-Mod/RimWorld-RimMind-Mod-Core

Length of output: 957


🏁 Script executed:

# Find where MaybeDispatch is called from
rg -nP --type=cs -C5 'MaybeDispatch\s*\('

Repository: RimWorld-RimMind-Mod/RimWorld-RimMind-Mod-Core

Length of output: 2756


🏁 Script executed:

# Check the full ResponseDispatcher implementation
cat -n Source/Npc/ResponseDispatcher.cs

Repository: RimWorld-RimMind-Mod/RimWorld-RimMind-Mod-Core

Length of output: 4063


🏁 Script executed:

# Search for all calls to ResponseDispatcher.Dispatch
rg -nP --type=cs 'ResponseDispatcher\.Dispatch'

Repository: RimWorld-RimMind-Mod/RimWorld-RimMind-Mod-Core

Length of output: 182


🏁 Script executed:

# Check if there's an async context in Player2StorageDriver where MaybeDispatch is called
rg -nP --type=cs -C10 'class Player2StorageDriver\|async.*{|\.Dispatch\(|MaybeDispatch' Source/Npc/Player2StorageDriver.cs | head -100

Repository: RimWorld-RimMind-Mod/RimWorld-RimMind-Mod-Core

Length of output: 3335


🏁 Script executed:

# Check the method signatures of ChatAsync and ChatSnapshot to confirm they're async
rg -nP --type=cs 'public.*Task.*Chat|async.*Task' Source/Npc/Player2StorageDriver.cs | head -20

Repository: RimWorld-RimMind-Mod/RimWorld-RimMind-Mod-Core

Length of output: 1064


🏁 Script executed:

# Check if LongEventHandler is already used anywhere in the codebase
rg -nP --type=cs 'LongEventHandler\.ExecuteWhenFinished'

Repository: RimWorld-RimMind-Mod/RimWorld-RimMind-Mod-Core

Length of output: 1759


Dispatch 在背景线程调用时违反主线程契约——需立即修复。

ResponseDispatcher.Dispatch 直接调用 RimWorld/Verse API(MoteMaker.ThrowText 第 38 行、Log.Warning/Log.Message 多处、bridge.Execute 第 73 行),这些 API 只能在主线程调用。而 MaybeDispatch 被调用于 ChatAsync 和 ChatAsync 等异步方法(第 103、162 行),这些方法运行在背景线程上。

根据编码规范「Never call RimWorld or Unity APIs from background threads; use LongEventHandler.ExecuteWhenFinished() to schedule callbacks on main thread」,需要在 MaybeDispatch 中用 LongEventHandler.ExecuteWhenFinished() 包装 ResponseDispatcher.Dispatch 调用,例如:

private void MaybeDispatch(NpcChatResult result, string npcId)
{
    if (!AutoDispatch) return;
    try
    {
        LongEventHandler.ExecuteWhenFinished(() => ResponseDispatcher.Dispatch(result));
    }
    catch (System.Exception ex)
    {
        AIRequestQueue.LogFromBackground($"[RimMind] Player2StorageDriver: auto-dispatch failed for '{npcId}' - {ex.Message}", isWarning: true);
    }
}

此模式在 AICoreAPI.cs、Player2Client.cs 等处已广泛使用,是项目标准做法。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Npc/ResponseDispatcher.cs` around lines 10 - 28,
ResponseDispatcher.Dispatch is being invoked from background threads (via
MaybeDispatch called by ChatAsync), which violates the main-thread-only
RimWorld/Unity API contract; wrap the Dispatch call inside
LongEventHandler.ExecuteWhenFinished in MaybeDispatch so all RimWorld/Verse
calls (e.g., MoteMaker.ThrowText, Log.Warning/Message, bridge.Execute inside
ResponseDispatcher.Dispatch) run on the main thread, and retain the existing
try/catch behavior by logging exceptions via AIRequestQueue.LogFromBackground
(keep the AutoDispatch check and pass the npcId context to the error log).

Comment on lines +50 to +63
string prevText = _inputText;
_inputText = Widgets.TextField(inputRect, _inputText);
bool inputFocused = GUI.GetNameOfFocusedControl() == "AgentDialogueInput";

if (Widgets.ButtonText(sendRect, "RimMind.Core.UI.AgentDialogue.Send".Translate()))
{
SendMessage();
}

if (Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Return && inputFocused)
{
SendMessage();
Event.current.Use();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

严重:Enter 发送从未生效。

第 52 行通过 GUI.GetNameOfFocusedControl() == "AgentDialogueInput" 判断焦点,但前文从未调用 GUI.SetNextControlName("AgentDialogueInput") 给 Widgets.TextField 命名,导致 inputFocused 始终为 false,Enter 键发送通道形同虚设。另外第 50 行 prevText 赋值但未使用。

🐛 建议修复
-            string prevText = _inputText;
+            GUI.SetNextControlName("AgentDialogueInput");
             _inputText = Widgets.TextField(inputRect, _inputText);
             bool inputFocused = GUI.GetNameOfFocusedControl() == "AgentDialogueInput";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
string prevText = _inputText;
_inputText = Widgets.TextField(inputRect, _inputText);
bool inputFocused = GUI.GetNameOfFocusedControl() == "AgentDialogueInput";
if (Widgets.ButtonText(sendRect, "RimMind.Core.UI.AgentDialogue.Send".Translate()))
{
SendMessage();
}
if (Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Return && inputFocused)
{
SendMessage();
Event.current.Use();
}
GUI.SetNextControlName("AgentDialogueInput");
_inputText = Widgets.TextField(inputRect, _inputText);
bool inputFocused = GUI.GetNameOfFocusedControl() == "AgentDialogueInput";
if (Widgets.ButtonText(sendRect, "RimMind.Core.UI.AgentDialogue.Send".Translate()))
{
SendMessage();
}
if (Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Return && inputFocused)
{
SendMessage();
Event.current.Use();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/UI/Window_AgentDialogue.cs` around lines 50 - 63, The Enter key never
triggers because the TextField is never named: before calling Widgets.TextField
you must call GUI.SetNextControlName("AgentDialogueInput") so
GUI.GetNameOfFocusedControl() can match; update the code around
Widgets.TextField/_inputText to call
GUI.SetNextControlName("AgentDialogueInput") immediately before
Widgets.TextField, keep the existing inputFocused check and SendMessage() calls
(including the KeyDown branch in Event.current), and remove the unused prevText
assignment (or use it for change detection) so there are no dead variables;
reference the symbols _inputText, Widgets.TextField, GUI.SetNextControlName,
inputFocused, SendMessage, and prevText when making the change.

coderabbitai[bot]
coderabbitai Bot previously requested changes Apr 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
Source/Core/AIRequestQueue.cs (1)

61-75: ⚠️ Potential issue | 🔴 Critical

切图/读档时取消在途请求后,旧任务的完成结果仍会再次回调。

StartedNewGame() / LoadedGame() 先给每个活动请求入队一个 Cancelled 响应并清空 _activeRequests,但后台 Task.Run 结束后还是会把原始 PendingFireResult 放进 _pendingFireResults。ProcessFireResult() 没有校验这个结果是否仍属于当前会话,所以同一个 RequestId 可能先收到 Cancelled,随后又收到旧世界的成功/失败结果。

建议给 TrackedRequest 增加会话代号或取消标记,并在 ProcessFireResult() 里先丢弃失效结果。

Also applies to: 77-91, 332-337, 341-383

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/AIRequestQueue.cs` around lines 61 - 75,
StartedNewGame/LoadedGame currently enqueue Cancelled responses and clear
_activeRequests but background tasks can later enqueue old PendingFireResult
into _pendingFireResults causing ProcessFireResult to apply stale results; add a
session-id or cancelled flag to TrackedRequest (e.g. a long SessionId or bool
IsCancelled) that is set when StartedNewGame/LoadedGame runs and set on all
TrackedRequest instances when you clear them, propagate that marker into
PendingFireResult produced by the background Task.Run, and update
ProcessFireResult to check the SessionId/IsCancelled on the incoming
PendingFireResult (or look up current TrackedRequest) and discard any result
whose session/flag does not match the current session so old-world results are
ignored (update usages around _pendingFireResults, _activeRequests,
TrackedRequest, ProcessFireResult).
Source/Client/Player2/Player2Client.cs (2)

55-71: ⚠️ Potential issue | 🟡 Minor

新加日志前缀还是 [RimMind]。

这几个新增日志没有跟仓库统一前缀保持一致,后续排查时会把 Core 日志和旧日志混在一起。统一改成 [RimMind-Core] 比较稳。
As per coding guidelines: "Use log prefix [RimMind-Core] for all logging statements" and "All log messages must use [RimMind-Core] prefix".

Also applies to: 279-287, 304-304, 320-320, 351-357, 393-393, 426-426

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Client/Player2/Player2Client.cs` around lines 55 - 71, Log messages
created in Player2Client are using the old prefix "[RimMind]"; update all
logging calls in this file (e.g., AIRequestQueue.LogFromBackground(...) usages
around the Player2Client factory/constructor and the catch block that logs
ex.Message) to use the standardized "[RimMind-Core]" prefix instead; ensure
ShowNotification calls and any other string literals that currently contain
"[RimMind]" are updated as well and apply the same change to the other listed
occurrences (lines near 279-287, 304, 320, 351-357, 393, 426) so every log
emitted by Player2Client uses "[RimMind-Core]".

207-213: ⚠️ Potential issue | 🔴 Critical

不要在后台轮询里读取 Current.Game。

这些 await Task.Delay(...) 之后的轮询代码会在后台线程继续执行,而 Current.Game 是 RimWorld API。这里把“游戏是否已卸载”的判断也放到了后台线程,已经踩到主线程边界了。建议改成由调用方传入取消信号,或在主线程上维护一个可读的停止标记。
Based on learnings: Main thread must read/write game state, consume ConcurrentQueue, and call all RimWorld/Unity APIs; background threads must only perform HTTP requests and JSON parsing with callbacks scheduled via LongEventHandler.ExecuteWhenFinished.

Also applies to: 250-253, 343-346, 387-389, 410-413, 468-470, 492-494, 516-518, 607-613

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Client/Player2/Player2Client.cs` around lines 207 - 213, The
background polling loop in Player2Client.cs (the while (!asyncOp.isDone) loop
that uses await Task.Delay and reads Current.Game and other RimWorld APIs) must
not access Current.Game from a background thread; change the polling methods to
accept a CancellationToken (or a simple volatile/atomic stop flag)
provided/updated by the caller on the main thread, replace direct Current.Game
checks inside the background loop with token.ThrowIfCancellationRequested() or
reads of that stop flag, and ensure any game-state checks or callbacks are
marshalled back to the main thread (e.g., via
LongEventHandler.ExecuteWhenFinished) — update the async loops that reference
asyncOp, webRequest.downloadedBytes, Current.Game and Task.Delay and apply the
same pattern to the other occurrences noted (around the symbols/methods at the
other listed locations).
Source/Core/Context/HistoryManager.cs (1)

178-178: ⚠️ Potential issue | 🟡 Minor

日志前缀不符合编码规范

🛠️ 建议修复
-                    Log.Message($"[RimMind] Key '{key.Key}' layer changed: {key.Layer} → {newLayer}");
+                    Log.Message($"[RimMind-Core] Key '{key.Key}' layer changed: {key.Layer} → {newLayer}");

As per coding guidelines: "Use log prefix [RimMind-Core] for all logging statements"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Context/HistoryManager.cs` at line 178, Logging in
HistoryManager.cs uses a non-conforming prefix; update the log statement(s) in
the HistoryManager class (e.g., the method containing the logging at the
reported location) to prepend the required prefix "[RimMind-Core]" to all
messages passed to the logger (e.g., processLogger, logger, or whichever
ILog/ILogger instance is used) so every call follows the coding guideline "Use
log prefix [RimMind-Core] for all logging statements."
Source/Debug/AICoreDebugActions.cs (1)

25-26: ⚠️ Potential issue | 🟡 Minor

日志前缀不符合编码规范

整个文件的日志均使用 [RimMind] 前缀,应统一为 [RimMind-Core]。

As per coding guidelines: "Use log prefix [RimMind-Core] for all logging statements"

Also applies to: 58-58, 72-72, 79-79, 86-87, 106-112, 121-122, 151-152, 158-158, 167-168, 202-203, 223-224, 248-248, 257-258, 264-265, 270-295, 302-343, 361-378, 393-408, 422-443

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Debug/AICoreDebugActions.cs` around lines 25 - 26, Replace the logging
prefix "[RimMind]" with "[RimMind-Core]" across all logging statements in
Source/Debug/AICoreDebugActions.cs (e.g., the Log.Warning(...) at the start and
all other Log.Debug/Log.Warning/Log.Error calls referenced in the review);
update each string literal used as the log message prefix in methods of the
AICoreDebugActions class so the exact prefix is "[RimMind-Core]" and preserve
the rest of each message and formatting.
♻️ Duplicate comments (7)
Source/Core/AgentBus/AgentBus.cs (1)

62-71: ⚠️ Potential issue | 🔴 Critical

后台事件派发仍然按泛型静态类型路由,具体事件订阅者会被跳过。

FlushBackgroundQueue() 这里传进去的是 AgentBusEvent,所以 DispatchToHandlers<T>() 里的 typeof(T) 会退化成基类;注册在 ActionEvent、GoalEvent 等具体类型上的处理器匹配不到。这和之前评论的是同一个根因,当前看还没修掉。

Also applies to: 77-98

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/AgentBus/AgentBus.cs` around lines 62 - 71, FlushBackgroundQueue
is dequeuing AgentBusEvent instances into _pendingDispatch and calling the
generic DispatchToHandlers<T>() with the compile-time type AgentBusEvent, which
prevents handlers registered for concrete types (e.g., ActionEvent, GoalEvent)
from matching; instead, invoke DispatchToHandlers using each event's runtime
type: for each evt in _pendingDispatch call the generic DispatchToHandlers<T>
with T = evt.GetType() (via a non-generic overload or by making a runtime
generic MethodInfo and invoking MakeGenericMethod(evt.GetType()).Invoke(...)),
or add/use a DispatchToHandlers(Type, AgentBusEvent) overload that dispatches
based on the runtime type; apply the same fix for the other identical loop (the
77–98 region).
Source/AICoreAPI.cs (1)

105-124: ⚠️ Potential issue | 🔴 Critical

await 之后仍在后台线程访问游戏状态。

driver.SpawnNpcAsync(profile) 返回后,NpcManager.Instance?.SpawnNpc(profile) 和 _contextEngine.BuildSnapshot(request) 都直接碰 RimWorld 状态;这里的 continuation 并不保证回到主线程。这个问题上个版本已经提过了,当前实现还在。请把这些游戏状态访问都切回 LongEventHandler.ExecuteWhenFinished(...)。
Based on learnings: Main thread must handle game state reads/writes, RimWorld/Unity API calls, and consume ConcurrentQueue; background threads are prohibited from calling RimWorld/Unity APIs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/AICoreAPI.cs` around lines 105 - 124, The Chat method is performing
RimWorld game-state operations on a background continuation after awaiting
driver.SpawnNpcAsync(profile) and when calling
NpcManager.Instance?.SpawnNpc(profile) and
_contextEngine.BuildSnapshot(request); move any code that reads/writes game
state (NpcManager.Instance?.SpawnNpc and the snapshot build) into a main-thread
callback by wrapping those calls inside
LongEventHandler.ExecuteWhenFinished(...) so they execute on the main thread
after the await completes, while leaving driver.SpawnNpcAsync(profile) awaited
on the background thread.
Source/UI/Window_AgentDialogue.cs (1)

48-61: ⚠️ Potential issue | 🔴 Critical

严重:Enter 键发送功能无法生效。

第 49 行调用 Widgets.TextField 前未调用 GUI.SetNextControlName("AgentDialogueInput"),导致 GUI.GetNameOfFocusedControl() 永远无法匹配,inputFocused 始终为 false,Enter 键发送通道失效。此外,第 48 行 prevText 赋值后从未使用。

🐛 建议修复
-            string prevText = _inputText;
+            GUI.SetNextControlName("AgentDialogueInput");
             _inputText = Widgets.TextField(inputRect, _inputText);
             bool inputFocused = GUI.GetNameOfFocusedControl() == "AgentDialogueInput";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/UI/Window_AgentDialogue.cs` around lines 48 - 61, The Enter-key send
check in Window_AgentDialogue fails because
GUI.SetNextControlName("AgentDialogueInput") is not called before
Widgets.TextField, so GUI.GetNameOfFocusedControl() never matches; also prevText
is assigned but unused—remove or use it. Fix by calling
GUI.SetNextControlName("AgentDialogueInput") immediately before the
Widgets.TextField call that sets _inputText in the Window_AgentDialogue UI code,
keep the inputFocused check using GUI.GetNameOfFocusedControl(), and either
remove the unused prevText variable or use it to detect changes to _inputText
before invoking SendMessage from the SendMessage method.
Source/Core/Flywheel/FlywheelParameterStore.cs (1)

18-26: ⚠️ Potential issue | 🔴 Critical

修复 GameComponent 构造器签名。

RimWorld 1.6 要求 GameComponent 子类必须提供 (Game game) : base(game) 形式的构造函数。当前两个构造函数都调用 : base() 而非 : base(game),导致组件的 game 字段未初始化,会造成生命周期失效。

🛡️ 建议修复
-        public FlywheelParameterStore() : base()
-        {
-            RegisterDefaults();
-        }
-
-        public FlywheelParameterStore(Game game) : base()
+        public FlywheelParameterStore(Game game) : base(game)
         {
             RegisterDefaults();
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Flywheel/FlywheelParameterStore.cs` around lines 18 - 26, The
GameComponent constructor signature is wrong: change the
FlywheelParameterStore(Game game) constructor to call base(game) instead of
base(), and remove (or avoid relying on) the parameterless
FlywheelParameterStore() constructor so the required GameComponent constructor
exists; update the constructors named FlywheelParameterStore() and
FlywheelParameterStore(Game game) so that the Game-taking constructor uses ":
base(game)" (and drop the parameterless ctor if not needed).
Source/Core/Flywheel/FlywheelGameComponent.cs (1)

21-22: ⚠️ Potential issue | 🔴 Critical

修复 GameComponent 构造器:必须传递 game 参数给基类。

RimWorld 1.6 要求 GameComponent 子类必须提供 (Game game) : base(game) 签名的构造函数。当前代码有两个构造器都调用 : base() 而未传递 game 参数,会导致自动实例化失败。

🛡️ 建议修复
-        public FlywheelGameComponent() : base() { }
-        public FlywheelGameComponent(Game game) : base() { }
+        public FlywheelGameComponent(Game game) : base(game) { }

Based on learnings: "GameComponent and WorldComponent must have correct constructor signature public ComponentType(Game game) for automatic RimWorld discovery"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Flywheel/FlywheelGameComponent.cs` around lines 21 - 22,
FlywheelGameComponent has an incorrect constructor signature: change the
constructor FlywheelGameComponent(Game game) to call the base class with the
game parameter (use : base(game)) so RimWorld can auto-instantiate, and remove
(or stop using) the parameterless constructor FlywheelGameComponent() to avoid
the incorrect base() call; ensure the class exposes the required public
FlywheelGameComponent(Game game) : base(game) constructor.
Source/Core/Context/BudgetScheduler.cs (1)

92-92: ⚠️ Potential issue | 🔴 Critical

Math.Clamp 在 .NET Framework 4.8 上不可用

System.Math.Clamp 仅在 .NET Core 2.0+ 中可用,在 net48 目标下会编译失败。项目其他文件已使用 UnityEngine.Mathf.Clamp 作为替代。

🛠️ 建议修复
+using UnityEngine;
 // ... 在方法中
-            float B = Math.Clamp(budget, 0f, 1f);
+            float B = Mathf.Clamp(budget, 0f, 1f);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Context/BudgetScheduler.cs` at line 92, The use of
System.Math.Clamp in BudgetScheduler.cs (float B = Math.Clamp(budget, 0f, 1f);)
is unavailable on .NET Framework 4.8; replace it with Unity's clamping API by
using UnityEngine.Mathf.Clamp (or fully qualify UnityEngine.Mathf.Clamp(budget,
0f, 1f)) so the BudgetScheduler class compiles under net48—update the statement
that assigns B accordingly and add or rely on the existing UnityEngine
using/import if needed.
Source/Core/Flywheel/FlywheelTelemetryCollector.cs (1)

1-7: ⚠️ Potential issue | 🔴 Critical

编译错误:缺少 using System.Linq

第 123 行调用了 kvp.Value.Count(v => v),这是 LINQ 扩展方法,但文件头部缺少 using System.Linq;,会导致编译失败。

🛠️ 建议修复
 using System;
 using System.Collections.Generic;
 using System.IO;
+using System.Linq;
 using Newtonsoft.Json;
 using RimMind.Core.Context;
 using RimMind.Core.Settings;
 using Verse;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Flywheel/FlywheelTelemetryCollector.cs` around lines 1 - 7, The
file is missing the System.Linq namespace required for LINQ extension methods;
add "using System.Linq;" to the using directives at the top of
FlywheelTelemetryCollector.cs so calls like kvp.Value.Count(v => v) compile;
locate the file's using block (contains System, Collections.Generic, IO,
Newtonsoft.Json, RimMind.Core.Context, RimMind.Core.Settings, Verse) and insert
the System.Linq using among them.
🟡 Minor comments (21)
Source/Core/JsonTagExtractor.cs-33-36 (1)

33-36: ⚠️ Potential issue | 🟡 Minor

把解析告警前缀统一成 [RimMind-Core]。

这些字符串会被 Source/AICoreMod.cs 直接转成实际日志输出;现在还是 [RimMind],后续按前缀筛日志时会把这类解析失败漏掉。

As per coding guidelines All log messages must use [RimMind-Core] prefix.

Also applies to: 53-53

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/JsonTagExtractor.cs` around lines 33 - 36, Update the log prefix
used in JsonTagExtractor to match the required standard: replace occurrences of
"[RimMind]" with "[RimMind-Core]" in the catch block inside the
JsonTagExtractor.Extract method (and the other log site around line 53
referenced in the comment). Specifically, change the Warn(...) calls in class
JsonTagExtractor so the message begins with "[RimMind-Core] JsonTagExtractor..."
instead of "[RimMind]" to ensure logs are recognized by Source/AICoreMod.cs.
Source/Core/AIRequestQueue.cs-162-163 (1)

162-163: ⚠️ Potential issue | 🟡 Minor

这一批新增日志前缀不一致。

这里混用了 [RimMind] 和 [RimMind][Core];仓库已经统一要求用 [RimMind-Core],不然之后做日志检索和问题归因会很碎。

As per coding guidelines All log messages must use [RimMind-Core] prefix.

Also applies to: 179-179, 208-208, 238-238, 282-284, 314-315, 373-380, 425-425, 501-501

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/AIRequestQueue.cs` around lines 162 - 163, Several Log.Message
calls in AIRequestQueue (e.g., the enqueue log referencing
request.RequestId/trackingId/modId and other logs around the noted ranges) use
inconsistent prefixes like "[RimMind]" or "[RimMind][Core]"; update all these
logging statements to use the canonical prefix "[RimMind-Core]". Search for
Log.Message occurrences in AIRequestQueue.cs (including the statements that
reference request.RequestId, trackingId, modId and the other logs listed in the
review) and replace the existing bracketed prefixes with "[RimMind-Core]" while
preserving the rest of each message text and interpolation.
Source/Core/Sensor/SensorManager.cs-25-46 (1)

25-46: ⚠️ Potential issue | 🟡 Minor

这里会把已注册的 sensor key 再注册一遍。

RimMindAPI.RegisterSensorProvider() 那边已经会调用 RegisterSensorContextKey(provider);FinalizeInit() 再跑一轮会把预先注册的 key 全部覆盖一次,并触发 ContextKeyRegistry 的覆盖 warning。建议只保留一个注册入口,或者先做幂等判断。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Sensor/SensorManager.cs` around lines 25 - 46, FinalizeInit
currently re-registers all sensors via RegisterSensorContextKeys causing
duplicate registrations and ContextKeyRegistry warnings; make registration
idempotent or remove this bulk registration. Update
FinalizeInit/RegisterSensorContextKeys to skip registering a key if it already
exists (check ContextKeyRegistry for key existence) or only register new
providers, or remove the FinalizeInit loop and rely solely on
RimMindAPI.RegisterSensorProvider which already calls
RegisterSensorContextKey(provider); refer to FinalizeInit,
RegisterSensorContextKeys, RimMindAPI.RegisterSensorProvider,
RegisterSensorContextKey, ContextKeyRegistry.Register and Sensor.SensorId to
locate and implement the change.
Source/Core/Sensor/SensorManager.cs-73-75 (1)

73-75: ⚠️ Potential issue | 🟡 Minor

日志前缀需要统一成 [RimMind-Core]。

这两个新增 warning 仍然用了旧前缀。
As per coding guidelines: "Use log prefix [RimMind-Core] for all logging statements" and "All log messages must use [RimMind-Core] prefix".

Also applies to: 102-104

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Sensor/SensorManager.cs` around lines 73 - 75, The catch blocks
in SensorManager.cs are using the old log prefix "[RimMind]" in the Log.Warning
calls (e.g., the one referencing sensor.SensorId) — update those warning
messages to use the mandated "[RimMind-Core]" prefix; locate the Log.Warning
calls in the SensorManager class (the catch handling around sensor.SensorId and
the other catch at the later block) and change the string prefix to
"[RimMind-Core]" so all log messages follow the coding guideline.
Source/AICoreAPI.cs-174-176 (1)

174-176: ⚠️ Potential issue | 🟡 Minor

新增日志前缀没有切到 [RimMind-Core]。

这批新日志还是旧前缀,建议统一一并改掉。
As per coding guidelines: "Use log prefix [RimMind-Core] for all logging statements" and "All log messages must use [RimMind-Core] prefix".

Also applies to: 257-267, 341-348, 424-425, 450-450, 474-474, 500-500, 522-522, 552-552

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/AICoreAPI.cs` around lines 174 - 176, Existing log calls use the old
"[RimMind]" prefix; update them to "[RimMind-Core]". In the shown block change
the AIRequestQueue.LogFromBackground call to use "[RimMind-Core]" and similarly
update every other log invocation flagged (e.g., the blocks around lines
referenced where AIRequestQueue.LogFromBackground and any other Log/Logger calls
occur) so all messages emitted before LongEventHandler.ExecuteWhenFinished /
onComplete?.Invoke(AIResponse.Failure(...)) use the new prefix; search for uses
of AIRequestQueue.LogFromBackground and other logging calls in the same
method/class and replace the prefix string consistently with "[RimMind-Core]".
INTRODUCTION.txt-81-87 (1)

81-87: ⚠️ Potential issue | 🟡 Minor

技术架构速览漏掉了当前实现里的 L5_Sensor 层。

现在 sensor provider 已经注册到 ContextLayer.L5_Sensor,这里仍把上下文层级写到 L4,会误导接入方理解实际的注入顺序。建议把这层补进速览说明里。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@INTRODUCTION.txt` around lines 81 - 87, Update the architecture summary to
include the missing L5_Sensor layer so it matches the current implementation:
add "L5_Sensor" into the ContextEngine layer sequence (e.g., make it "L0(静态) →
L1(基线+Diff) → L2(环境) → L3(状态) → L4(历史) → L5_Sensor"), and mention that sensor
providers are registered at ContextLayer.L5_Sensor (and/or via
ContextKeyRegistry.Register with that layer) so readers see the actual injection
order used by the code.
Source/Core/Context/ContextEngine.cs-300-300 (1)

300-300: ⚠️ Potential issue | 🟡 Minor

日志前缀不符合规范。

应使用 [RimMind-Core] 前缀。

🛠️ 建议修复
-            catch (Exception ex) { Log.Warning($"[RimMind] Embedding snapshot failed: {ex.Message}"); }
+            catch (Exception ex) { Log.Warning($"[RimMind-Core] Embedding snapshot failed: {ex.Message}"); }

-                Log.Message($"[RimMind] Budget trim applied for {snapshot.NpcId}: ...");
+                Log.Message($"[RimMind-Core] Budget trim applied for {snapshot.NpcId}: ...");

Also applies to: 713-713

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Context/ContextEngine.cs` at line 300, Update the log prefix used
in the ContextEngine catch handlers from "[RimMind]" to the standardized
"[RimMind-Core]"; locate the Log.Warning/Log.* calls inside the ContextEngine
class (e.g., the catch in the embedding snapshot handler and the other
occurrence around the second reported spot) and change the literal prefix in the
formatted message while preserving the rest of the message and exception
interpolation.
Source/Core/Flywheel/FlywheelRuleEngine.cs-146-146 (1)

146-146: ⚠️ Potential issue | 🟡 Minor

日志前缀不符合规范。

多处日志使用 [RimMind] 前缀,应统一使用 [RimMind-Core]。

🛠️ 建议修复
-                        Log.Message($"[RimMind] Flywheel recommendation: ...");
+                        Log.Message($"[RimMind-Core] Flywheel recommendation: ...");

-                        Log.Message($"[RimMind] Flywheel auto-applied: ...");
+                        Log.Message($"[RimMind-Core] Flywheel auto-applied: ...");

-                catch (Exception ex) { Log.Warning($"[RimMind] ParameterTuner '{tuner.TunerId}' error: {ex.Message}"); }
+                catch (Exception ex) { Log.Warning($"[RimMind-Core] ParameterTuner '{tuner.TunerId}' error: {ex.Message}"); }

Also applies to: 156-156, 164-164

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Flywheel/FlywheelRuleEngine.cs` at line 146, Update all
Log.Message calls in FlywheelRuleEngine to use the standardized prefix
"[RimMind-Core]" instead of "[RimMind]"; specifically find the logging
statements in the FlywheelRuleEngine class (the lines emitting recommendations
like the one referencing rec.Target/rec.CurrentValue/rec.RecommendedValue and
similar messages at the occurrences around the existing Log.Message calls) and
replace the prefix string so the log lines consistently start with
"[RimMind-Core]". Ensure no other parts of the message are altered and run a
quick search in this class for any remaining "[RimMind]" literals to update
them.
Source/Core/Context/ScenarioRegistry.cs-36-36 (1)

36-36: ⚠️ Potential issue | 🟡 Minor

日志前缀不符合规范。

🛠️ 建议修复
-                Log.Warning($"[RimMind] Scenario '{scenarioId}' already registered, overwriting.");
+                Log.Warning($"[RimMind-Core] Scenario '{scenarioId}' already registered, overwriting.");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Context/ScenarioRegistry.cs` at line 36, Log message uses a
hardcoded prefix "[RimMind]" that doesn't follow the project's logging prefix
convention; replace the literal prefix in the Log.Warning call inside
ScenarioRegistry (the line using scenarioId) with the project's standard logging
prefix constant or helper (e.g., use the shared LogPrefix constant or a logging
helper method) so the warning reads the same as other logs and keeps formatting
consistent.
Source/Core/Flywheel/FlywheelGameComponent.cs-48-48 (1)

48-48: ⚠️ Potential issue | 🟡 Minor

日志前缀不符合规范。

🛠️ 建议修复
-                catch (Exception ex) { Log.Warning($"[RimMind] Flywheel analysis failed: {ex.Message}"); }
+                catch (Exception ex) { Log.Warning($"[RimMind-Core] Flywheel analysis failed: {ex.Message}"); }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Flywheel/FlywheelGameComponent.cs` at line 48, 在
FlywheelGameComponent.cs 中定位含有 catch (Exception ex) { Log.Warning($"[RimMind]
Flywheel analysis failed: {ex.Message}"); } 的捕获块,替换日志前缀为项目规范的前缀(例如
"RimMind:")并保留原始错误信息,改为 Log.Warning($"RimMind: Flywheel analysis failed:
{ex.Message}"); 以确保前缀格式与统一规范一致。
Source/Core/Flywheel/FlywheelParameterStore.cs-67-67 (1)

67-67: ⚠️ Potential issue | 🟡 Minor

日志前缀不符合规范。

🛠️ 建议修复
-                    Log.Message($"[RimMind] FlywheelParameterStore: {key} = {value} (was {old})");
+                    Log.Message($"[RimMind-Core] FlywheelParameterStore: {key} = {value} (was {old})");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Flywheel/FlywheelParameterStore.cs` at line 67, 日志前缀硬编码为
"[RimMind]" 不符合项目规范;在 FlywheelParameterStore 中替换该硬编码前缀,改为使用统一的日志前缀源(例如现有的静态常量
LogPrefix 或动态使用 nameof(FlywheelParameterStore) / 类级别常量),并更新 Log.Message 调用
(那一行包含 key, value, old) 以使用该统一前缀格式,从而确保日志前缀一致且可维护;如果没有现成的常量,新增类级别的 private const
string LogPrefix = "YourStandardPrefix" 并引用它。
Source/UI/Window_AgentDialogue.cs-164-164 (1)

164-164: ⚠️ Potential issue | 🟡 Minor

日志前缀不符合规范。

应使用 [RimMind-Core] 前缀而非 [RimMind]。

🛠️ 建议修复
-                    Log.Warning($"[RimMind] AgentDialogue chat failed: {ex.Message}");
+                    Log.Warning($"[RimMind-Core] AgentDialogue chat failed: {ex.Message}");

As per coding guidelines: "Use log prefix [RimMind-Core] for all logging statements"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/UI/Window_AgentDialogue.cs` at line 164, In Window_AgentDialogue.cs
update the logging prefix used in the catch block where Log.Warning($"[RimMind]
AgentDialogue chat failed: {ex.Message}") is called (inside the
AgentDialogue-related method of class Window_AgentDialogue) to use the
standardized prefix "[RimMind-Core]" instead of "[RimMind]" so the message
becomes "[RimMind-Core] AgentDialogue chat failed: {ex.Message}".
Source/Core/Flywheel/FlywheelRuleEngine.cs-126-126 (1)

126-126: ⚠️ Potential issue | 🟡 Minor

潜在空引用风险。

store 可能为 null(当 FlywheelParameterStore.Instance 未初始化时),但此处使用 store! 强制非空断言传递给 ApplyAutoApplyMode。应添加空检查或在方法内部处理。

🛠️ 建议修复
-            ApplyAutoApplyMode(report, store!);
+            if (store != null)
+                ApplyAutoApplyMode(report, store);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Flywheel/FlywheelRuleEngine.cs` at line 126, ApplyAutoApplyMode
is being called with a forced non-null store (store!) but
FlywheelParameterStore.Instance may be null; add a null-check before calling
ApplyAutoApplyMode (e.g., if store == null then skip call or initialize/retrieve
a fallback) or modify ApplyAutoApplyMode to accept a nullable parameter
(FlywheelParameterStore?) and handle null internally; update callers and method
signature accordingly and use the symbol names ApplyAutoApplyMode, store, and
FlywheelParameterStore.Instance to locate the change.
Source/Core/Context/ContextEngine.cs-283-298 (1)

283-298: ⚠️ Potential issue | 🟡 Minor

潜在空引用:Vector 字段可能为 null。

当 SemanticEmbedding.GetBlockEmbedding() 返回 null 且 key.KeyEmbedding 也为 null 时,vector 仍为 null,但 EmbeddingSnapshotRecord.Vector 被赋值为 vector!(强制非空)。这可能导致后续处理出现空引用异常。

🛠️ 建议修复
                     float[]? vector = SemanticEmbedding.GetBlockEmbedding(request.NpcId, key.Key);
                     if (vector == null && key.KeyEmbedding != null)
                         vector = key.KeyEmbedding;
+                    if (vector == null) continue; // Skip if no embedding available
                     float relevanceScore = snapshot.KeyScores.TryGetValue(key.Key, out var score) ? score : 0f;
                     _embeddingSnapshotStore.Record(new EmbeddingSnapshotRecord
                     {
                         ...
-                        Vector = vector!,
+                        Vector = vector,
                         ...
                     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Context/ContextEngine.cs` around lines 283 - 298, The
EmbeddingSnapshotRecord is being saved with a forced non-null Vector (vector!)
even when SemanticEmbedding.GetBlockEmbedding(...) and key.KeyEmbedding are both
null; update ContextEngine so you only call _embeddingSnapshotStore.Record when
a non-null vector exists or provide a safe default (e.g. Array.Empty<float>())
instead of forcing non-null. Concretely, after computing float[]? vector from
SemanticEmbedding.GetBlockEmbedding(request.NpcId, key.Key) and
key.KeyEmbedding, add a null-check and either skip calling
_embeddingSnapshotStore.Record for that key or assign a non-null sentinel before
constructing EmbeddingSnapshotRecord.Vector to prevent null reference issues.
Source/Core/Agent/PawnAgent.cs-191-191 (1)

191-191: ⚠️ Potential issue | 🟡 Minor

日志前缀不符合编码规范

多处日志使用 [RimMind] 前缀,应统一为 [RimMind-Core]。

As per coding guidelines: "Use log prefix [RimMind-Core] for all logging statements"

Also applies to: 204-204, 234-234, 334-334

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Agent/PawnAgent.cs` at line 191, The log prefix "[RimMind]" is
used in PawnAgent's logging (e.g., the Log.Warning call inside
HandleThinkResponse and other lines at the same file), but coding guidelines
require "[RimMind-Core]"; update all occurrences in PawnAgent.cs (notably the
Log.Warning at the HandleThinkResponse catch and the other mentioned lines
around 204, 234, 334) to use "[RimMind-Core]" while keeping the rest of the
message unchanged to preserve context (e.g., replace $"[RimMind]
HandleThinkResponse parse failed..." with $"[RimMind-Core] HandleThinkResponse
parse failed..." and do analogous replacements for the other log statements).
Source/Npc/HybridStorageDriver.cs-32-32 (1)

32-32: ⚠️ Potential issue | 🟡 Minor

日志前缀不符合编码规范

所有 AIRequestQueue.LogFromBackground 调用使用 [RimMind] 前缀,应统一为 [RimMind-Core]。

As per coding guidelines: "Use log prefix [RimMind-Core] for all logging statements"

Also applies to: 40-40, 57-57, 70-70, 83-83, 93-93, 100-100, 109-109, 116-116, 125-125, 132-132, 141-141, 149-149

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Npc/HybridStorageDriver.cs` at line 32, Update all logging calls in
HybridStorageDriver to use the standardized prefix: replace "[RimMind]" with
"[RimMind-Core]" in every AIRequestQueue.LogFromBackground invocation within the
HybridStorageDriver class (locations include the calls at/around the remote
SpawnNpc and the other catch blocks listed). Search for
AIRequestQueue.LogFromBackground usages in HybridStorageDriver.cs and modify the
message strings to begin with "[RimMind-Core]" so the prefix conforms to the
coding guideline.
Source/Npc/Player2StorageDriver.cs-69-69 (1)

69-69: ⚠️ Potential issue | 🟡 Minor

日志前缀不符合编码规范

多处日志使用 [RimMind] 前缀,应统一使用 [RimMind-Core]。涉及行:69, 79, 117, 203, 224, 234, 244, 258, 333。

As per coding guidelines: "Use log prefix [RimMind-Core] for all logging statements"

Also applies to: 79-79, 117-117, 203-204, 224-224, 234-234, 244-244, 258-258, 333-333

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Npc/Player2StorageDriver.cs` at line 69, The log prefix "[RimMind]" is
not following the coding guideline; update all logging calls in the
Player2StorageDriver class to use "[RimMind-Core]" instead. Locate the logging
statements (e.g., inside SpawnNpcAsync catch block and other methods referenced)
and replace the prefix in Log.Warning / Log.Error / Log.Info calls so messages
like $"[RimMind] ..." become $"[RimMind-Core] ..."; ensure all occurrences on
the reported lines (and any other "[RimMind]" uses in Player2StorageDriver) are
changed consistently.
Source/Npc/LocalStorageDriver.cs-200-200 (1)

200-200: ⚠️ Potential issue | 🟡 Minor

日志前缀不符合编码规范

🛠️ 建议修复
-            catch (Exception ex) { Log.Warning($"[RimMind] Failed to extract message from JSON: {ex.Message}"); }
+            catch (Exception ex) { Log.Warning($"[RimMind-Core] Failed to extract message from JSON: {ex.Message}"); }

As per coding guidelines: "Use log prefix [RimMind-Core] for all logging statements"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Npc/LocalStorageDriver.cs` at line 200, Update the logging prefix in
the catch block inside LocalStorageDriver where Log.Warning is called (the catch
that currently logs "[RimMind] Failed to extract message from JSON:
{ex.Message}"); change the prefix to "[RimMind-Core]" so the message becomes
"[RimMind-Core] Failed to extract message from JSON: {ex.Message}". Locate the
Log.Warning call in the LocalStorageDriver class and replace the old prefix with
the required "[RimMind-Core]" prefix.
Source/Core/Flywheel/FlywheelTelemetryCollector.cs-101-101 (1)

101-101: ⚠️ Potential issue | 🟡 Minor

日志前缀不符合编码规范

日志使用 [RimMind] 前缀,但编码规范要求使用 [RimMind-Core]。

🛠️ 建议修复
-                Log.Warning($"[RimMind] Telemetry flush failed: {ex.Message}");
+                Log.Warning($"[RimMind-Core] Telemetry flush failed: {ex.Message}");

As per coding guidelines: "Use log prefix [RimMind-Core] for all logging statements"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Flywheel/FlywheelTelemetryCollector.cs` at line 101, 在
FlywheelTelemetryCollector.cs 中定位触发该日志的代码(查找 Log.Warning(...) 调用、错误处理块或 Flush
相关方法),将日志前缀从 "[RimMind]" 更改为规范前缀 "[RimMind-Core]",即更新该 Log.Warning($"[RimMind]
Telemetry flush failed: {ex.Message}") 调用使消息使用
"[RimMind-Core]",并保持原有错误文本和变量不变以避免更改语义。
Source/Core/Agent/PawnAgent.cs-257-260 (1)

257-260: ⚠️ Potential issue | 🟡 Minor

空 catch 块吞没异常

catch 块仅 return 而未记录任何日志或错误信息,会导致问题难以排查。

🛠️ 建议修复
             catch
             {
+                Log.Warning($"[RimMind-Core] PawnAgent RequestToolFeedback: failed to parse toolCallsJson");
                 return;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Agent/PawnAgent.cs` around lines 257 - 260, The empty catch in
PawnAgent swallows exceptions; replace it with a catch(Exception ex) that logs
the error before returning (e.g. use the class's logger or Console/Error
logging): change the bare catch in the PawnAgent class to catch(Exception ex) {
/* logger.LogError(ex, "Contextual message about where this failed"); */ return;
} so exceptions are recorded (use the existing logging facility in PawnAgent if
available, otherwise Console.Error.WriteLine/ex.ToString()).
Source/Core/Context/BudgetScheduler.cs-178-178 (1)

178-178: ⚠️ Potential issue | 🟡 Minor

日志前缀不符合编码规范

🛠️ 建议修复
-                    Log.Message($"[RimMind] Key '{key.Key}' layer changed: {key.Layer} → {newLayer}");
+                    Log.Message($"[RimMind-Core] Key '{key.Key}' layer changed: {key.Layer} → {newLayer}");

As per coding guidelines: "Use log prefix [RimMind-Core] for all logging statements"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Context/BudgetScheduler.cs` at line 178, The log message prefix
is incorrect in the BudgetScheduler logging statement; update the Log.Message
call in BudgetScheduler (the line that currently logs "[RimMind] Key '{key.Key}'
layer changed: {key.Layer} → {newLayer}") to use the required prefix
"[RimMind-Core]" so the message becomes "[RimMind-Core] Key '{key.Key}' layer
changed: {key.Layer} → {newLayer}". Ensure no other parts of the string are
altered and keep the interpolation variables intact.
🧹 Nitpick comments (5)
Source/Core/Flywheel/FlywheelRuleEngine.cs (1)

187-192: 避免硬编码参数值。

budgetLimit 计算使用硬编码的 4000f,应使用 FlywheelParameterStore.Instance?.TotalBudget ?? 4000 或从 TelemetryRecord 中获取实际预算值,以保持与参数存储的一致性。

♻️ 建议修复
                 if (r.BudgetValue > 0 && r.TotalTokens > 0)
                 {
-                    float budgetLimit = r.BudgetValue * 4000f;
+                    int totalBudget = FlywheelParameterStore.Instance?.TotalBudget ?? 4000;
+                    float budgetLimit = r.BudgetValue * totalBudget;
                     if (budgetLimit > 0)

As per coding guidelines: "use FlywheelParameterStore.TotalBudget"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Flywheel/FlywheelRuleEngine.cs` around lines 187 - 192, Replace
the hard-coded 4000f used to compute budgetLimit in FlywheelRuleEngine (inside
the block checking r.BudgetValue and r.TotalTokens) with the configured
TotalBudget value; read it via FlywheelParameterStore.Instance?.TotalBudget ??
4000 (cast/convert to float as needed) or use the budget value present on the
TelemetryRecord if available, so budgetLimit = r.BudgetValue *
(configuredTotalBudget) instead of r.BudgetValue * 4000f; update the calculation
near where sum += r.TotalTokens / budgetLimit is computed.
Source/Npc/Player2StorageDriver.cs (1)

324-335: MaybeDispatch 未传递 pawn 参数给 ResponseDispatcher

根据 ResponseDispatcher.Dispatch 的定义(见 Source/Npc/ResponseDispatcher.cs),它接受可选的 Pawn? pawn 参数用于显示气泡文字和执行命令。当前调用未传递 pawn,可能导致消息无法正确显示在对应 NPC 上。

♻️ 建议修复
 private void MaybeDispatch(NpcChatResult result, string npcId)
 {
     if (!AutoDispatch) return;
     try
     {
-        ResponseDispatcher.Dispatch(result);
+        var pawn = NpcManager.FindPawnByNpcId(npcId);
+        ResponseDispatcher.Dispatch(result, pawn);
     }
     catch (System.Exception ex)
     {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Npc/Player2StorageDriver.cs` around lines 324 - 335, MaybeDispatch
currently calls ResponseDispatcher.Dispatch(result) without the optional Pawn
parameter, so the NPC bubble/commands won't be associated with the correct pawn;
update MaybeDispatch to accept a Pawn? pawn (or otherwise obtain the Pawn from
npcId) and call ResponseDispatcher.Dispatch(result, pawn), and update all
callers of MaybeDispatch in Player2StorageDriver to pass the pawn (or null where
appropriate) so the dispatcher can display messages and run commands on the
correct Pawn.
Source/Core/Context/BudgetScheduler.cs (1)

39-64: OnParameterChanged 方法未被调用

此方法定义了参数变更的处理逻辑,但类内部从未调用。如果计划用于 FlywheelParameterStore 的回调订阅,请确保注册;否则建议移除。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Context/BudgetScheduler.cs` around lines 39 - 64,
OnParameterChanged is never invoked; either register it as the parameter-change
callback with the FlywheelParameterStore (e.g., subscribe in the BudgetScheduler
constructor or Init method by calling the store's subscription method and
passing OnParameterChanged) so changes update _config.W1/W2/Alpha/etc., or
delete this unused method if subscriptions aren't needed; update any
constructor/initialization code that creates or holds FlywheelParameterStore to
call the appropriate subscribe/unsubscribe methods referencing
OnParameterChanged.
Source/Core/Flywheel/FlywheelTelemetryCollector.cs (1)

105-126: ComputeCacheHitRates 方法未被调用

此方法定义为 private static,但在类内部从未被调用,属于死代码。如果计划在 telemetry 记录时使用,请在 Record 方法中调用;否则建议移除。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Flywheel/FlywheelTelemetryCollector.cs` around lines 105 - 126,
The private static method ComputeCacheHitRates is dead code because it's never
used; either call it from the telemetry path or remove it. If you intend to
record cache hit rates, call ComputeCacheHitRates(snapshot) inside the class's
Record method (or the telemetry emission method used there) and include the
returned Dictionary<string,float>? in the telemetry payload (guarding for null);
otherwise delete ComputeCacheHitRates to remove unused code. Ensure you
reference the existing symbol name ComputeCacheHitRates and the Record method
when making the change.
Source/Core/Context/HistoryManager.cs (1)

19-21: 锁粒度过粗:所有 NPC 历史共享同一把锁

当前使用单一 _listLock 保护所有 NPC 的历史列表操作。当不同 NPC 的历史被并发访问时,会产生不必要的锁竞争。建议改用每个列表独立的锁对象。

♻️ 建议方案
-        private readonly object _listLock = new object();
+        // 每个 NPC 历史列表使用独立锁,存储在包装类中
+        private class LockedHistoryList
+        {
+            public readonly List<HistoryEntry> Entries = new List<HistoryEntry>();
+            public readonly object Lock = new object();
+        }
+
+        private readonly ConcurrentDictionary<string, LockedHistoryList> _histories =
+            new ConcurrentDictionary<string, LockedHistoryList>();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Core/Context/HistoryManager.cs` around lines 19 - 21, The current
single _listLock causes unnecessary contention across all NPC histories; replace
it with a per-history lock strategy by introducing a
ConcurrentDictionary<string, object> (e.g., _listLocks) and use
_listLocks.GetOrAdd(id, _ => new object()) to lock only the specific NPC list
when mutating or iterating the List<HistoryEntry> stored in _histories; update
all places that currently lock(_listLock) (or manipulate _histories entries) to
acquire the per-key lock for the corresponding history id, ensure new lists are
created via _histories.GetOrAdd(id, _ => new List<HistoryEntry>()), and
optionally remove the lock entry from _listLocks when a history is deleted to
avoid leakage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 394fa8f5-f68d-4472-a0c0-3178622d3d2f

📥 Commits

Reviewing files that changed from the base of the PR and between 7034299 and 686af14.

⛔ Files ignored due to path filters (3)
  • AGENTS.md is excluded by !**/*.md
  • CLAUDE.md is excluded by !**/*.md
  • README.md is excluded by !**/*.md
📒 Files selected for processing (41)
  • INTRODUCTION.txt
  • Languages/ChineseSimplified/Keyed/RimMind_Core.xml
  • Languages/English/Keyed/RimMind_Core.xml
  • Source/AICoreAPI.cs
  • Source/AICoreMod.cs
  • Source/Client/AIResponse.cs
  • Source/Client/Player2/Player2Client.cs
  • Source/Core/AIDebugLog.cs
  • Source/Core/AIRequestQueue.cs
  • Source/Core/Agent/PawnAgent.cs
  • Source/Core/AgentBus/AgentBus.cs
  • Source/Core/Context/BudgetScheduleResult.cs
  • Source/Core/Context/BudgetScheduler.cs
  • Source/Core/Context/ContextDiff.cs
  • Source/Core/Context/ContextEngine.cs
  • Source/Core/Context/ContextKeyRegistry.cs
  • Source/Core/Context/ContextLayer.cs
  • Source/Core/Context/ContextSnapshot.cs
  • Source/Core/Context/HistoryGameComponent.cs
  • Source/Core/Context/HistoryManager.cs
  • Source/Core/Context/ScenarioRegistry.cs
  • Source/Core/Context/SchemaRegistry.cs
  • Source/Core/Extensions/ISensorProvider.cs
  • Source/Core/Extensions/IStreamingResponseHandler.cs
  • Source/Core/Extensions/Sensor/AgentToolDefinition.cs
  • Source/Core/Flywheel/FlywheelBuiltinTuner.cs
  • Source/Core/Flywheel/FlywheelGameComponent.cs
  • Source/Core/Flywheel/FlywheelParameterStore.cs
  • Source/Core/Flywheel/FlywheelRuleEngine.cs
  • Source/Core/Flywheel/FlywheelTelemetryCollector.cs
  • Source/Core/JsonTagExtractor.cs
  • Source/Core/Sensor/SensorManager.cs
  • Source/Debug/AICoreDebugActions.cs
  • Source/Npc/HybridStorageDriver.cs
  • Source/Npc/IStorageDriver.cs
  • Source/Npc/LocalStorageDriver.cs
  • Source/Npc/Player2StorageDriver.cs
  • Source/Settings/AICoreSettings.cs
  • Source/UI/AICoreSettingsUI.cs
  • Source/UI/Window_AgentDialogue.cs
  • Tests/RimMindAPISkipCheckTests.cs
✅ Files skipped from review due to trivial changes (3)
  • Source/Core/Extensions/IStreamingResponseHandler.cs
  • Source/Core/Context/SchemaRegistry.cs
  • Source/Npc/IStorageDriver.cs
🚧 Files skipped from review as they are similar to previous changes (6)
  • Source/Core/Context/ContextLayer.cs
  • Source/Core/Extensions/ISensorProvider.cs
  • Source/Core/Context/ContextKeyRegistry.cs
  • Source/Client/AIResponse.cs
  • Source/UI/AICoreSettingsUI.cs
  • Source/Core/Context/BudgetScheduleResult.cs

Comment thread Source/AICoreAPI.cs Outdated
Comment thread Source/AICoreMod.cs Outdated
Comment thread Source/Core/Agent/PawnAgent.cs Outdated
Comment thread Source/Core/AgentBus/AgentBus.cs Outdated
Comment thread Source/Core/Context/ContextSnapshot.cs Outdated
Comment thread Source/Core/Extensions/Sensor/AgentToolDefinition.cs Outdated
Comment on lines +103 to +104
var engine = RimMindAPI.GetContextEngine();
var snapshot = engine.BuildSnapshot(request);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

潜在空引用异常

RimMindAPI.GetContextEngine() 可能返回 null,但代码未检查直接调用了 BuildSnapshot,会导致 NullReferenceException。

🛠️ 建议修复
             var engine = RimMindAPI.GetContextEngine();
+            if (engine == null)
+            {
+                Log.Warning("[RimMind-Core] ContextEngine not initialized.");
+                return;
+            }
             var snapshot = engine.BuildSnapshot(request);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var engine = RimMindAPI.GetContextEngine();
var snapshot = engine.BuildSnapshot(request);
var engine = RimMindAPI.GetContextEngine();
if (engine == null)
{
Log.Warning("[RimMind-Core] ContextEngine not initialized.");
return;
}
var snapshot = engine.BuildSnapshot(request);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Debug/AICoreDebugActions.cs` around lines 103 - 104,
RimMindAPI.GetContextEngine() can return null causing a NullReferenceException
when calling BuildSnapshot(request); update the code in AICoreDebugActions
(around where engine is assigned) to null-check the result of
RimMindAPI.GetContextEngine() before calling engine.BuildSnapshot(request), and
handle the null case (e.g., log an error via the existing logger, return early,
or throw a clear exception) so BuildSnapshot is only invoked on a non-null
engine.

Comment on lines +105 to +106
var engine = RimMindAPI.GetContextEngine();
var snapshot = engine.BuildSnapshot(request);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

潜在空引用异常

RimMindAPI.GetContextEngine() 可能返回 null,直接调用 BuildSnapshot 会导致 NullReferenceException。

🛠️ 建议修复
             var engine = RimMindAPI.GetContextEngine();
+            if (engine == null)
+                return new NpcChatResult { Error = "Context engine not initialized." };
             var snapshot = engine.BuildSnapshot(request);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Npc/LocalStorageDriver.cs` around lines 105 - 106,
RimMindAPI.GetContextEngine() can return null so avoid calling BuildSnapshot on
a null reference: in LocalStorageDriver where you call var engine =
RimMindAPI.GetContextEngine(); check engine for null before calling
engine.BuildSnapshot(request) and handle it (e.g., log an error on the
driver/logger, return a failure/result indicating the missing engine, or throw a
clear exception). Ensure you reference the engine variable and the BuildSnapshot
call so the null-check covers that exact call path and choose a consistent
error-handling strategy used elsewhere in this class.

Comment on lines +121 to +122
var engine = RimMindAPI.GetContextEngine();
var snapshot = engine.BuildSnapshot(request);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

同上:ChatStreamingAsync 中也需要检查 engine 是否为 null

🛠️ 建议修复
             var engine = RimMindAPI.GetContextEngine();
+            if (engine == null)
+                return new NpcChatResult { Error = "Context engine not initialized." };
             var snapshot = engine.BuildSnapshot(request);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var engine = RimMindAPI.GetContextEngine();
var snapshot = engine.BuildSnapshot(request);
var engine = RimMindAPI.GetContextEngine();
if (engine == null)
return new NpcChatResult { Error = "Context engine not initialized." };
var snapshot = engine.BuildSnapshot(request);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Source/Npc/LocalStorageDriver.cs` around lines 121 - 122, 在
ChatStreamingAsync 方法中对 engine 进行 null 检查:在调用 RimMindAPI.GetContextEngine() 后验证
engine != null,若为 null 则返回合适的错误/异常或提前结束流(与其它方法的行为一致),并避免继续调用
engine.BuildSnapshot(request);请定位
ChatStreamingAsync、RimMindAPI.GetContextEngine、engine 和 snapshot
标识符并在同一方法内加入该防护分支以保证不会在 engine 为 null 时访问 BuildSnapshot。

Comment thread Tests/RimMindAPISkipCheckTests.cs Outdated
Comment on lines +11 to +33
private static readonly ConcurrentDictionary<string, Func<object, string, bool>> _skipChecks
= new ConcurrentDictionary<string, Func<object, string, bool>>();

private static void Register(string sourceId, Func<object, string, bool> check)
=> _skipChecks[sourceId] = check;

private static void Unregister(string sourceId)
=> _skipChecks.TryRemove(sourceId, out _);

private static bool ShouldSkip(object target, string triggerType)
{
foreach (var check in _skipChecks.Values.ToList())
{
try
{
if (check(target, triggerType)) return true;
}
catch (Exception)
{
}
}
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

这些测试在测一份“复制实现”,不是在测 RimMindAPI。

Register / Unregister / ShouldSkip 全是测试文件里的私有实现,所以 Source/AICoreAPI.cs 真正的注册、覆盖、异常吞掉和迭代逻辑即使回归,这组测试也会继续全绿。建议直接走 RimMindAPI.RegisterDialogueSkipCheck() 等公开 API,或者把注册表逻辑下沉成一个可复用的纯类后再测试那个类。

Based on learnings Register SkipCheck handlers with RimMindAPI.RegisterDialogueSkipCheck(), RegisterFloatMenuSkipCheck(), or RegisterActionSkipCheck() to implement mutual exclusion between modules.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/RimMindAPISkipCheckTests.cs` around lines 11 - 33,
当前测试直接验证了测试文件内的私有注册表实现 (Register/Unregister/ShouldSkip) 而不是 RimMindAPI
的真实行为;请改为通过公共接口调用或提取可复用类:更新 Tests/RimMindAPISkipCheckTests.cs,删除对私有
ConcurrentDictionary-based 实现的直接断言,改用 RimMindAPI.RegisterDialogueSkipCheck(),
RimMindAPI.RegisterFloatMenuSkipCheck(), 或 RimMindAPI.RegisterActionSkipCheck()
来注册临时 handler,验证通过 RimMindAPI 的公开行为(注册、覆盖、异常处理、迭代跳过)或将当前注册表逻辑提取为一个独立可测试的类(例如
DialogueSkipRegistry)并在测试中直接引用该类以替代测试文件内的私有方法。

@mcocdaa

mcocdaa commented May 7, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

mcocdaa added 25 commits May 14, 2026 06:34
refactor: move source files to backup directory for architecture restructuring
feat(arch): update architecture tests for Jason Taylor Clean Architecture restructuring

Update all architecture test files to reflect the new project structure following Jason Taylor Clean Architecture pattern (Domain/Application/Infrastructure/Presentation). Key changes include:

- Add `backup` directory exclusion to all file scanning operations
- Update file paths and namespace checks to match new directory layout
- Expand allowed class patterns in Domain purity tests to include value objects, exceptions, and caches
- Add new whitelist entries for duplicate type/file name checks
- Update legacy directory checks to cover Contracts, Kernel, Core, and Adapters
- Restrict public interface locations to Application/Common/Interfaces only
- Skip future-phase directory existence tests for Colony, AgentMode, and Unified Pipeline
- Add new GameComponent whitelist entries for Flywheel, AgentBus, and AIRequestQueue
refactor: remove unused streaming and fix nullable warnings

- Remove `ChatStreamingAsync` from `IStorageDriver` interface
- Remove unused `Krafs.Rimworld.Ref` package reference
- Initialize string fields to `string.Empty` and nullable fields to `null`
- Suppress CS0649 and CS0618 warnings for DTOs and deprecated API
- Handle null values in `NpcProfileExposure` with null-coalescing
refactor(architecture): reorganize namespaces and move dead code to backup

Restructure the codebase by moving classes to appropriate layers (Core -> Infrastructure/Presentation/Application) and relocating unused files to a backup directory. Update all XML defs, language keys, and references accordingly.

Add integration test project with initial test infrastructure and stubs for future testing.
refactor: remove dead code and add InternalsVisibleTo for integration tests
refactor: update namespace references and exclude backup_dead_code from arch tests

Update XML defs to use `RimMind.Infrastructure.Patches` and `RimMind.Presentation.Agent` namespaces. Exclude `backup_dead_code` directory from architecture tests and skip the TransientExceptionChecker test as it was moved to dead code.
refactor(architecture): replace static enum with string-based provider identifiers and introduce factory pattern

Migrate AI provider identification from enum to string constants to support extensibility. Introduce `IAIClientFactory` interface and implementations for OpenAI and Player2 providers, enabling dynamic client creation via service locator. Extract mechanism interfaces into separate concerns (metadata, reader, writer, trigger) for better separation of responsibilities. Add `IOpenAISettings` interface to decouple OpenAI client from concrete settings class. Move `AgentState` enum to domain layer and add `ContextPreset` enum. Register core services in `DependencyInjection` classes and wire them through `RimMindServiceLocator`.
refactor(architecture): split IGameMechanism into separate interfaces and migrate settings to DI

- Split monolithic IGameMechanism into IMechanismReader, IMechanismWriter, IMechanismTrigger, and IMechanismMetadata for better separation of concerns
- Convert ISettingsProvider and IContextSettings properties to read-write with setters
- Migrate settings UI to use ISettingsProvider via DI instead of static RimMindCoreMod.Settings
- Add IWindowService abstraction for opening agent dialogue and request log windows
- Move IPawnAgent interface to Application.Common.Interfaces.Agent namespace
- Remove dead code: IPawnDataExtractor, IRelevanceProvider, IKernelParameterTuner
- Add SerializeAgent method to IPawnAgentFactory for proper save/load support
- Add ApplyPreset and ResetToDefault methods to IContextSettings
- Add IsOpenAIConfigured method to ISettingsProvider
- Add custom prompt fields and flywheel auto-apply settings to ISettingsProvider
- Update arch tests to reflect new interface names
- Fix test references to use correct namespaces
refactor(architecture): replace IEventBus with IAgentBus and extract settings interfaces

Split monolithic IContextSettings and ISettingsProvider into focused interfaces (IAIModelSettings, IAgentTickSettings, IApiCredentialSettings, ICircuitBreakerSettings, IContextBudgetSettings, IContextCalibrationSettings, IContextEnvironmentSettings, IContextIncludeSettings, IDebugSettings, IFlywheelSettings, IOverlaySettings, IPromptSettings, IQueueSettings). Remove IEventBus in favor of IAgentBus, eliminating the SimpleEventBusAdapter. Introduce MutablePipeline and PipelineFactory for pipeline construction. Update all consumers to use the new interfaces and remove direct service locator calls from middleware.
refactor(settings): split IContextSettings into focused interfaces

Replace the monolithic IContextSettings interface with separate interfaces for budget, environment, and include settings to improve separation of concerns and maintainability.
feat(architecture): implement agent mode system with reactive and proactive strategies

Add the Agent Mode architecture as the new core decision-making framework, replacing the legacy bus-injected agent pattern. Introduce `IAgentMode` and `IThinkStrategy` interfaces with two built-in implementations: ReactiveAgentMode (responds to perception events) and ProactiveAgentMode (periodic self-evaluation). Update PawnAgent to hold a current mode, support mode switching via `SwitchMode()`, and persist mode ID across saves. Refactor PawnThinker to delegate thinking to the current mode's strategy, removing direct AI request construction. Clean up the codebase by deleting the entire `Source/backup/` directory (dead code from v1.x), removing `HistoryEntry` from NpcTypes, and stripping `IAgentBus` from PawnAgent constructor. Register built-in modes at runtime initialization and expose them via `RimMindAPI.Modes`. Add comprehensive architecture tests (Phase I) for namespace, format, and interface contracts.
feat(architecture): migrate agent modes to application layer and improve DI

Move agent mode interfaces and implementations from Presentation to Application layer, establishing proper architectural boundaries. Introduce IAgentInfo interface to decouple agent state access, and refactor PawnAgent, PawnThinker, and ContextOrchestrator to use constructor injection instead of service locator. Add AgentBusCoreSubscriber for centralized event logging, implement IContextSettings on SettingsProvider and AIRequestQueueImpl, and update pipeline factories to accept explicit dependencies. Remove old Presentation agent mode files and update project references accordingly.
refactor(architecture): restructure agent system and relocate UI to presentation layer

- Extract IAgentControl interface from IPawnAgent for cleaner separation
- Add IAgentFactory and IPawnAgentFactory with constructor injection
- Move settings UI from Infrastructure to Presentation namespace
- Introduce Null defaults for extension interfaces
- Add ProviderHelper for dynamic provider resolution
- Implement IAIRequestQueueTickable for queue lifecycle management
- Add ITelemetryCollector and IFlywheelRuleEngine interfaces
- Rename IPawnDataExtractor to PawnExtractedData model
- Move ParameterRecommendation to Models namespace
- Add IJsonExtractor interface and implementation
- Update AgentModeId with namespaced values and Normalize method
- Remove ToolCall and ToolResult event types
- Add LogFromBackground method to ILogSink
- Add RequiresApiKey to IAIClientFactory
- Add GetSaveSnapshot/LoadFromSnapshot to IFlywheelParameterStore
- Move dead code to backup directory
refactor(architecture): restructure namespaces and decouple service dependencies

- Move AgentIdentity to Application layer for sub-mod compatibility
- Introduce IJobProvider and IPlayer2Lifecycle abstractions
- Replace static service locator calls with injected dependencies
- Add thread-safe middleware pipeline with volatile snapshot pattern
- Implement JsonRepairHelper for truncated JSON recovery
- Relocate dead code to backup directory and remove unused interfaces
- Update localization keys to simplified namespace hierarchy
- Add SchemaRegistry for dynamic output schema registration
- Improve FlywheelRuleEngine with token usage ratio analysis
- Fix BusPublishContext middleware ordering for error isolation
feat(architecture): refactor core architecture with SRP decomposition and extension system

Split RimMindRuntime into dedicated managers (CompositionRoot, LifecycleManager, ExtensionManager) to satisfy Single Responsibility Principle. Refactor GameContextBuilder into PawnContextBuilder and MapContextBuilder. Migrate pipeline contexts from Presentation to Application layer. Introduce IAgentState, IAgentLifecycle, IAgentBehavior interfaces as sub-interfaces of IAgentControl. Add OwnerModId to IExtension for mod ownership tracking. Implement lazy middleware merging in MutablePipeline. Split Player2Client into partial files by concern. Rename HttpHelper to HttpTransport, ProviderHelper to AIProviderRegistry, JsonHelpers to JsonExtractorUtils, JsonRepairHelper to JsonRepairer, JobActionHandlers to JobActionDispatcher. Add RimMindDefaults for centralized constants. Add UnregisterByOwner to ExtensionRegistry. Update all middlewares and extensions with OwnerModId.
feat(architecture): add reflection, planning, and inner voice systems with major refactoring

- Introduce IReflectionStrategy, IDailyPlanner, and InnerVoiceHandler for agent self-reflection and daily planning
- Add domain models: ReflectionEntry, ScheduleBlock, NeedLevel, RimMindDynamicThought, and new event types
- Implement DefaultReflectionStrategy and DefaultDailyPlanner skeletons with tick-based scheduling
- Add Null Object implementations for extension interfaces (IDialogueTrigger, IIncidentExecutedListener, etc.)
- Extract PawnContextBuilder.Extract partial class and MechanismToolHelper for DRY
- Move AgentBusCoreSubscriber to Common.Defaults namespace
- Replace hardcoded constants with centralized RimMindDefaults
- Split SettingsProvider into partial files by concern
- Add QueueCircuitBreaker and QueueDefaultSettings for queue management
- Add ContextBuildServices parameter object to reduce constructor overload
- Add IPawnPerceiver, IPawnThinker, IPawnActor, IPawnRecorder interfaces
- Add Destroy() to IAgentLifecycle for proper agent cleanup
- Add DispatchAction delegate to IAgentBus for pipeline integration
- Add TryCreateHybridStorageDriver to IClientManager
- Add RecordAction to IFlywheelParameterStore
- Add middleware execution order constants to RimMindDefaults
- Add arch tests for Phase I+ reflection/planning rules
- Remove dead code: ThreatClassifier keyword logic, static facade methods, NpcProfileBuilder, RimMindAPI.Sensors
- Fix thread affinity check to throw on non-main thread
- Fix HybridStorageDriver and Player2StorageDriver to use StorageDriverDependencies
- Fix OpenAIClient to use partial class for request building
- Fix RimMindRuntime extension registry to use ServiceLocator as single source of truth
- Fix PawnAgent serialization to rebuild collaborators after load
- Fix AIDebugLog to remove unused static Instance accessor
- Fix RequestOverlay to resolve IOverlaySettings directly
feat: implement unified request pipeline, psychology & social systems, and K-phase architecture refactor

- Introduce unified LlmRequestEnvelope/LlmResponse replacing AIRequest/AIResponse
- Add unified pipeline with 12 middleware components (ShortCircuit, TraceContext, NpcEnrich, ContextBuild, ContextFeedback, RequestSanitize, Cache, Telemetry, CircuitBreaker, Retry, ClientInvoke, ToolCallDispatch)
- Implement psychology system: IPsychologyWatcher, IThoughtInjector, IPawnPsychologyDataProvider with Verse implementations
- Implement social emergence system: IInformationDiffuser, ISocialEventOrganizer, ITraitEvolutionEngine, IDreamGenerator, ISleepDetector
- Add 7-dimension BudgetScheduler scoring (Priority, SceneRelevance, QuerySimilarity, Recency, UseFeedback, UserPin, CooldownPenalty)
- Implement IRelevanceLearner with RingBuffer-based feedback tracking
- Add ProviderCache for async context providers with staleness and invalidation
- Implement IContextKeyRegistry (instance-based) replacing static ContextKeyRegistry
- Add ContextProviderDef for async provider registration
- Implement IRemoteSyncService replacing IStorageDriver for KV operations
- Add ChunkAggregator for streaming response accumulation
- Implement IActionExecutor with DecisionMapper for Mechanism-based action execution
- Add AgentWorkflowPhase-driven Perceive→Think→Act→Record cycle
- Implement ThinkContextEnricher for InnerVoice, psychology alerts, and behavior history feedback
- Add ProactiveBehaviorExecutor for reflection, planning, dream, and trait evolution extensions
- Implement streaming support in OpenAIClient and Player2Client
- Add HybridAIClient for remote-first with local fallback
- Add RemoteSyncOrchestrator with debounced push and last-write-wins strategy
- Add 17 built-in Mechanism registrations in CompositionRoot
- Add RemoteSyncSettingsUI and NpcSyncActions UI components
- Add Phase H2, I, K, L, N, O arch tests
- Add comprehensive unit tests for all new components
- Remove obsolete IStorageDriver, ContextRequest, AIRequest, AIResponse, NpcChatResult types
- Remove static ContextKeyRegistry and RelevanceTable in favor of instance-based implementations
- Remove obsolete pipeline factories (AI, Npc, Context) in favor of unified pipeline
Players can now cycle agent modes (Reactive/Proactive) via a Gizmo on active pawns. Only shown when more than one mode is registered.

feat: add integration tests, debug windows, and clean architecture compliance

- Add integration tests for all mechanisms, pipeline, and circuit breaker
- Add debug windows for tool calls, mechanism status, and agent mode
- Extract IAgentBus into IEventPublisher, IEventSubscriber, and IAgentBusAdministration
- Add IAgentIdentityProvider, IDreamThoughtInjector, and ITraitEvolver interfaces
- Move RemoteSyncSettingsUI from Infrastructure to Presentation layer
- Fix layer dependency violations in ProactiveBehaviorExecutor, PawnAgentFactory, and FloatMenu_InnerVoice
- Add TraceContext restoration in PawnThinker callback path
- Add ChineseSimplified and English translations for new UI elements
SwitchMode now shows a RimWorld message and logs the mode change with pawn name, old mode, and new mode. Also fills the timestamp in AgentModeChangedEvent.

feat(agent): add mode switch notification with timestamp

Add player notification and timestamp tracking when agent mode changes. The SwitchMode method now logs the change, passes a timestamp to AgentModeChangedEvent, and displays a translated message to the player via Messages.Message, guarded by a Current.Game null check.

Add corresponding language entries for both English and Chinese Simplified locales, along with structural tests verifying the new behavior.
…ck to GetDebugInfo

GetDebugInfo now includes the current agent mode ID, perception buffer entry count, and last think tick for better runtime debugging.

feat(agent): add CurrentModeId, PerceptionBuffer, and LastThinkTick to debug info

Extend PawnAgent.GetDebugInfo to include three additional fields for improved runtime debugging. Add corresponding source-file-based tests to verify the output contains these fields without requiring Verse dependencies.
feat(agent): migrate logging from Verse.Log to ILogSink

Replace direct calls to `Verse.Log` with the `ILogSink` abstraction across the Presentation/Agent layer. Inject `ILogSink` into `PawnAgent`, `PawnAgentFactory`, `PawnThinker`, and `ProactiveBehaviorExecutor` to enable traceId propagation and testable logging.

Add architecture tests to verify the migration is complete and no direct `Verse.Log` calls remain in the target classes.
feat(architecture): add agent mode context and comprehensive test infrastructure

Add `AgentModeId` property to `LlmRequestContext` for mode-aware middleware, implement `IProactiveExtensions` interface in `ProactiveAgentMode`, and introduce runtime autotests for H2/K/L architecture verification. Improve mode cycling logic in `CompPawnAgent` to skip non-applicable modes.

Establish dedicated test stubs in `ApplicationStubs.cs` and add comprehensive unit tests for inner voice, daily planning, psychology watching, reflection, dream generation, information diffusion, social event organization, and trait evolution. Include domain tests for `LlmRequestEnvelopeBuilder`, `ContextDiff`, `EmbedCache`, and `KeyMeta`. Add presentation-layer tests verifying `ProactiveBehaviorExecutor` uses `IProactiveExtensions` interface.

Clean up test project by removing references to deleted source files and consolidating global usings.
refactor(openai): extract request serialization into standalone class

Move the JSON request building logic from `OpenAIClient.RequestBuilding.cs` into a new static `OpenAIRequestSerializer` class. This enables unit testing and snapshot verification of the exact wire request without depending on the client's dependencies.

Also re-enable test compilation by adding back source file includes in the test project.
Mode strategy/transition policy, perception sources, unified pipeline
updates, envelope/context changes, and accompanying ArchTests.

feat: implement request quality snapshot with agentic loop, guardrails, and perception sources

Add comprehensive request quality improvements including:
- Agentic loop service with configurable max depth for tool call rounds
- Input guardrails (empty perception, budget overflow, repetition detection)
- Output guardrail middleware for empty responses and repetitive actions
- Decision processor extracted from PawnThinker for single responsibility
- Decision validator for action intent format and parameter validation
- Proactive behavior orchestrator for reflection, planning, dream, and trait evolution
- Envelope enrichment coordinator and behavior history enricher
- Perception source framework with 6 built-in sources (mood, health, combat, need, social, environment)
- Mode transition policy interface with default allow-all policy
- Agent autonomy levels (Manual, Guided, Autonomous, Full) with risk-based approval
- GameStateInfo structured data model replacing raw string
- Few-shot examples in think strategies for improved AI output quality
- XML-wrapped context layers for better prompt structure
- Span tracing infrastructure for diagnostics
- Conversation compactor for history management
- ITab_Pawn_Agent UI showing status, goals, strategy weights, and behavior history
- Gizmo improvements: FloatMenu mode selection, pause/resume, emergency stop, force think
- Structured logging format across all components
- Serialization completion for last think tick and autonomy level
- AgentBus event type registration API and WorkflowPhaseChange event
- AddMiddleware bug fix supporting both bus and LLM pipelines
test: add failing test for decision envelope with sentinel schema
…ngs.RimMindCoreSettings

feat(settings): add legacy RimMindCoreSettings compatibility shim

Add a shim class in the RimMind.Core.Settings namespace that inherits
from RimMind.Presentation.Settings.RimMindCoreSettings. This allows
RimWorld's Scribe to resolve the legacy type name when loading saved
configurations, avoiding "Could not find class" errors during migration
to the new namespace.
…r active tabs

refactor(ui): use standard RimWorld tab styling in page host drawer

Replace custom tab background, accent line, and hover overlay with
Widgets.ButtonText default rendering and DrawHighlightSelected for the
active tab, matching vanilla RimWorld tab appearance.

Update UI drawing contract to assert the HighlightSelected call instead
of per-button text colors, and add a DrawHighlightSelected stub.
…vice view

feat(ui): add provider presets and extended model service support

Add a provider preset menu to the API settings tab with one-click
configuration for OpenCode Go, DeepSeek, SiliconFlow, Moonshot, Ollama,
and OpenAI official endpoints, plus a quick-apply button for OpenCode Go.

Add an extended_service provider path with a dedicated status section
showing configured endpoints, active node count, balancing strategy, and
primary endpoint, resolved via reflection from the ModelService mod.

Handle extended_service in the connection test flow and treat player2 and
extended_service as not requiring an API key when no factory is
registered. Add localization keys for both English and Chinese and
extend UI contracts to cover the new paths.
refactor(ui): replace inline description labels with tooltip helpers

Settings tabs previously rendered descriptions as separate gray indented
labels below each control, cluttering the layout. Introduce
LabelWithTooltip, SliderWithTooltip, and TextEntryWithTooltip extension
methods in SettingsUIDrawer and route existing labels, sliders, and text
entries through them so descriptions appear on hover instead.

DrawSectionHeader and DrawCustomPromptSection now accept an optional
tooltip, letting tab-level descriptions attach to their headers rather
than occupying standalone rows. Add the missing flywheel auto-apply
description string to both language files and extend the UI lifecycle
contract to assert the new helpers are used.
…rce hover tooltip standard

refactor(ui): rework settings drawers with inline labels and text areas

Replace LabelWithTooltip/SliderWithTooltip rows in ApiTabDrawer with
explicit label, description and slider widgets so descriptions render
inline instead of only on hover. Switch custom prompt editors in
PromptsTabDrawer and SettingsUIDrawer to Widgets.TextArea and persist
prompt changes immediately. Simplify PromptsTabDrawer scroll layout to
use the input rect directly. Extend the UI lifecycle contract test to
cover the new prompt section behavior.
…ground

refactor(ui): center reset buttons and remove bottom bar background

Center the reset-to-default buttons in the context tab drawer and
settings bottom bar using explicit button dimensions, and drop the
solid background fill behind the settings bottom bar for a cleaner
look.

Update UI lifecycle contract tests to assert the removed background
draw call and the new centering calculation.
…Dev menu

feat(debug): add test live ai request debug action

Add a debug action that opens the context payload inspector,
providing a shortcut to test live AI requests via the payload
inspector window.
… provider selector, and revamp debug center

feat(ui): overhaul debug center overview and settings UI

Rework the Debug Center overview page into a live control hub with
sections for AI connectivity probing, queue/cooldown controls, runtime
settings tuning, and a colonist agent inspector. Add LiveAiProbeState to
run background connectivity probes with latency, token, and response
reporting, plus an offline simulation mode.

Consolidate settings section headers into SettingsUIDrawer with a shared
card-style header and tooltip support, and drop the duplicated
label/description pairs across the API, context, and prompts tabs.
Replace the provider preset menu with a trimmed list and remove the
OpenCode Go quick-apply bar.

Add a view mode selector and reset buttons to the prompts tab, and
tooltips to context presets and queue actions. Update tab host rendering
to draw the selected tab with an atlas background instead of a highlight
overlay. Extend test stubs and contracts to cover the new drawing paths.
…API key affordance

feat(ui): improve API key field and settings layout

- Add placeholder text for empty API key fields and allow clicking
  the masked field to reveal and edit the key
- Render a consistent bordered background for API key fields
- Split slider labels and values in the debug overview page so
  values display separately with accent color
- Adjust section header height and text positioning for better
  vertical alignment
- Add EmptyPlaceholder localization strings for English and
  Chinese Simplified
…pressed states

refactor(ui): unify tab and preset button rendering with active state

Replace ad-hoc DrawBoxSolid/ButtonText combinations with a shared
pattern that draws Widgets.ButtonBGAtlasClick plus a centered label
for the active item and falls back to Widgets.ButtonText otherwise.
This gives selected tabs, presets, and view modes a consistent
pressed appearance across the settings, context, prompts, and debug
windows.

Remove the transient preset/provider feedback banner and its backing
state from ApiTabDrawer, deriving the active preset from current
settings instead. Update UiInteractionDriver checks to assert the
pressed active state rather than the removed visual banner.
…pages

fix(ui): prevent text overflow and improve tooltips in agent and debug panels

Truncate long labels to their available width and add tooltips
showing the full text across the agent activity stream, detail
panel, list panel, and debug center overview. Widen the detail
panel status area and agent list rows to accommodate the layout,
and align the status badge and primary state button correctly.

Add missing Verse stubs (LabelCap, ToStringFull, StringEx.Truncate)
required by the updated UI code and tests.
…ode Go requests

feat(clients): attach x-opencode-session header for OpenCode endpoints

Add EnsureOpenCodeSessionHeader to HttpTransport to inject an
x-opencode-session header when the request targets an OpenCode URL or
uses an oc_sk_ API key. OpenAIClient now detects OpenCode Go endpoints
and derives a stable session id from the request envelope (NpcId,
RequestId, or TraceId), falling back to a generated id. Streaming and
non-streaming paths both apply the header.

Add contract tests covering URL-based detection, key-based detection
with a custom session id, and absence of the header for standard
OpenAI endpoints.
…roviders to external modules

refactor(ui): drive provider menu from factory metadata

Extend IAIClientFactory with DisplayLabel, DefaultEndpoint,
DefaultModelName, OrderWeight and VisibleInMenu so provider presets and
menu entries are supplied by the registered factories instead of being
hardcoded in the settings UI.

ApiTabDrawer now builds the provider selection menu from the registry,
ordered by OrderWeight, and resolves the active provider label through
the matching factory. The OpenCode Go special case and the reflection
based ModelService status summary are removed; the extended service
section now only checks whether the provider is registered.

Add contract tests covering the new factory metadata.
…tract assertions

test: fix malformed contract tests and stale UI assertions

Split the AIClientFactory metadata contract into its own [Fact] method
so the OpenCode session header test is no longer nested inside another
test body, and drop the duplicate method declaration.

Remove obsolete UiLifecycleContract assertions that referenced the old
DrawExtendedServiceSection signature and the removed "OpenCodeGo"
literal, keeping the contract aligned with the current UI API.
…w rendering

feat(lookup): add thread-safe pawn cache and main-thread guards

Introduce a ConcurrentDictionary-backed cache in RimMindPawnLookup so
pawns can be resolved off the main thread without touching Find.Maps or
mapPawns, which trigger list pooling errors in RimWorld 1.6. Lookups now
return null off-thread when a pawn is not cached, and GetEligibleColonists
yields nothing outside the main thread.

Populate the cache from NpcManager indexing and from successful lookups,
and route CoreContextProviders.ResolvePawn through the shared lookup to
unify resolution logic.

Remove an accidental duplicate Widgets.Label call in the agent activity
stream drawer that rendered trace rows twice, and drop obsolete provider
preset and model service localization keys.

Add contract tests covering the thread-safety boundary, explicit caching,
and the single-label rendering invariant.
…ooltip card

feat(agents): enrich request trace rows with parsed summaries and tooltips

Parse JSON responses to extract reply/narration text and thought
descriptions, clean XML tags and newlines from prompts, and localize
tool call summaries. Add a rich tooltip detail field to trace rows
showing status, latency, tokens, model, source, intent, tools, content
and error, and wire it into the activity stream drawer. Add contract
tests covering JSON extraction, prompt cleaning, tool call formatting
and markdown code block handling.
…gue activity stream display

feat(trace): record tool calls and dialogue in request traces

Persist tool call traces when a request completes by parsing the
response's ToolCallsJson, so tool invocations are visible even when
the model returns no text content. Fall back to ToolCallsJson for the
trace response content when Content is empty.

Add a Function property to StructuredToolCall to support deserializing
OpenAI-style nested function payloads.

Deduplicate tool call traces by ToolCallId so repeated updates replace
the existing entry instead of appending duplicates.

Render express_dialogue tool calls as the extracted speech and optional
thought in the agents page trace rows.
…ace deduplication

feat(agents): extract dialogue from express_dialogue tool call args

Add TryExtractDialogueToolCall to parse speech and thought from
express_dialogue tool call arguments, handling both JSON object and
array response shapes as well as escaped JSON string arguments. Fall
back to a regex match on the raw speech field when JSON parsing fails.

Exclude express_dialogue from the tool call summary so dialogue rows
no longer leak the tool name or call id, and return an empty summary
when no other tools were invoked.
…ministic serialization

feat(context): reorder prompt into 4-zone cache-optimized layout

Restructure context assembly to maximize KV-cache prefix hits across
turns. Messages are now emitted in a strict 4-zone order: immutable
static prefix (L0), semi-static agent profile (L1), append-only
conversation history (L4), and a volatile tail (L2/L3/L5 plus the
current query). Previously history was appended after all layers,
interleaving volatile observations into the prefix and invalidating
the cache on every tick.

Supporting changes:
- PromptBudget.Compose preserves the original chronological sequence
  when trimming instead of re-sorting by priority, so dropped sections
  never reorder the stable prefix.
- ContextLayerBuilder and OpenAIRequestSerializer sort entries and
  tools by ordinal key/name for deterministic, byte-identical output.
- BudgetScheduler.ChooseLayer no longer downgrades declared layers
  based on cumulative budget.
- Inspector window reports prefix/volatile token split, estimated
  cache hit rate, and tail isolation, with new localized strings.
- Add PromptCacheOptimizationContracts covering zone ordering, prefix
  byte-stability across ticks, deterministic tool sorting, and
  chronological trimming; update async build contracts for the new
  ordering.
…n with real LLM toolcalls and KV-cache tracking

feat(debug): add 10-day playthrough autotest runner

Add TenDayPlaythroughRunner, a headless debug harness that drives a
10 in-game day colony simulation to exercise the LLM pipeline end to
end. Each day runs four phases (morning thought, social dialogue,
advisor proposal, night reflection), advancing the tick manager at
ultrafast speed and auto-dismissing incident dialogs.

The runner captures per-day colonist snapshots, token usage, prefix
cache hit ratio and milestone screenshots, writing a JSON report plus
a Markdown chronicle to the save data folder. Credentials can be
overridden via RIMMIND_TEST_API_KEY, RIMMIND_TEST_ENDPOINT and
RIMMIND_TEST_MODEL environment variables.

BehaviorAutotestRunner now dispatches to the playthrough runner when
the rimmind-10day-playthrough command line arg is present.
… facade

feat(settings): add activity frequency scale and extend playthrough runner

Introduce a unified ActivityFrequencyScale setting (0.1x–3.5x) that
controls the trigger rate of colonist thoughts, social dialogue, and
advisor suggestions, exposed via API, settings provider, and both the
mod options and debug center UI. Presets now set a matching scale.

Lower default agent tick intervals and mod cooldown to make activity
more responsive, and add a case-insensitive fallback lookup for mod
cooldowns in the queue circuit breaker.

Generalize the 10-day playthrough runner to accept an arbitrary day
count via command line or environment variable, extract colonist
survival/sustenance helpers, vary morning and social prompts by day,
and write reports under both the day-count and legacy file names.
…and modal auto-dismissal

feat(playthrough): add autonomous agent action phase and tool argument extraction

Add an afternoon phase to the 10-day playthrough runner where a colonist
agent autonomously selects one of four tools (prioritize_work, take_job,
eat_and_recreation, stabilize_rest) via an LLM decision request, recorded
in the day report and chronicle.

Introduce ExtractToolArguments to robustly parse tool call JSON across
multiple shapes (bare objects, arrays, nested function/arguments, fenced
code blocks, string-encoded arguments), replacing duplicated ad-hoc
parsing in the morning thought and dialogue phases.

Also add varied fallback texts for morning thoughts and dialogue, clear
request cooldowns during queue cleanup, dismiss naming and log windows
during the run, and fix duplicate local declarations in the debug center
page and API tab drawer.
…gent activation, and dynamic frequency scaling

feat: add auto-activation of colonist agents

Add an AutoActivateColonistAgents setting that automatically starts
autonomous perception and decision loops for new or joining colonists,
removing the need to manually activate each pawn in the agent panel.

- Expose the setting through IAgentTickSettings and the settings provider
- Auto-transition dormant colonist agents to active in CompPawnAgent tick
- Add an "Activate All Agents" button to the debug center overview page
- Dispatch autonomous actions through the real agent pipeline in the
  ten-day playthrough runner and apply physiological feedback for
  composite mechanisms
- Add English and Chinese localization strings
refactor(ui): remove redundant primary state button draw call

The primary button is already rendered by the subsequent draw logic
in the same switch case, making the DrawPrimaryStateButton call
redundant and causing duplicate rendering.
…e workflow)

build: pin Newtonsoft.Json and add runtime dependency verification

Pin Newtonsoft.Json to 13.0.4 with GeneratePathProperty and copy the
net45 assembly to the output folder so the mod ships a known-good
runtime dependency instead of relying on version floating.

Add a DeployToModsFolder target (opt-in via RIMWORLD_DIR) to mirror
About, Defs, Languages and version folders into the game mods
directory after build.

Add RuntimeReferenceTests to assert the built assembly references
Newtonsoft.Json, does not reference System.Net.Http, and that the
packaged Newtonsoft.Json.dll is version 13.0.0.0.

Extend the release workflow to set up .NET 10 and build the mod
before determining the git range.

Also remove duplicate local declarations in the debug center drawer
and settings provider left over from earlier edits.
…BBCode

docs: add bilingual overview and setup guide to INTRODUCTION

Expand INTRODUCTION.txt with structured Chinese and English
sections covering core features, requirements, and load order
for RimMind-Core.
ci: add dependabot config for github actions updates
@mcocdaa
mcocdaa merged commit 6b71b90 into main Sep 24, 2026
2 checks passed
@mcocdaa
mcocdaa deleted the feat/architecture branch September 24, 2026 11:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant