Repository navigation
feat(core): merge architecture improvements into main - #10
Conversation
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Repository: RimWorld-RimMind-Mod/RimWorld-RimMind-Mod-Core/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: Walkthrough引入完整的NPC代理子系统、分层上下文引擎、感知管道、事件总线、飞轮遥测/规则引擎、嵌入与历史管理、多个存储驱动和大量公共API扩展;并更新本地化、文档与设置 UI。 Changes
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: 更新历史与显示响应
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: 返回候选目标
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 分钟 Possibly related PRs
Suggested labels
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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()调用 - 首次使用时返回 nullAICoreDebugActions.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 存在多个问题:
文件位置违规:Patch 文件应位于
Source/Patch/目录,而非Source/Npc/。代码库中所有其他 Harmony patch 均遵循此约定(如Source/Patch/AITogglePatch.cs等)。缺少 Harmony ID:根据编码指南,所有 Harmony patch 必须包含属性
[HarmonyID("mcocdaa.RimMindCore")]。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 | 🟠 MajorStoryteller / 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 usingConcurrentDictionary」,此处也应改用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
ConcurrentDictionarywith 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")而不做归一化。这会导致:
- 初始/订阅时若 store 中
w1+w2 ≠ 1,调度器会以非归一化权重运行;而Schedule中的阈值threshold = 1.0f - B(第 97 行)和评分w1*P + w2*E(第 106 行)的语义都假设w1+w2=1,否则筛选行为会偏离预期。- 触发任一参数变更后又自动归一化,行为前后不一致。
建议统一策略:要么始终把
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:207Source/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与降级链路丢失结构化契约。
- Line 259 与 Line 267 出现
catch { },完全吞掉异常。这与本 PR 的目标"replace empty catches with warnings"直接冲突;遥测异常被静默后会让 Flywheel 数据缺口非常难定位。- 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
⛔ Files ignored due to path filters (6)
.github/scripts/analyze_release.pyis excluded by!.github/**.github/workflows/pr-review.ymlis excluded by!.github/**.github/workflows/release.ymlis excluded by!.github/**AGENTS.mdis excluded by!**/*.mdREADME.mdis excluded by!**/*.mdscript/deploy-single.shis excluded by!script/**
📒 Files selected for processing (112)
.coderabbit.yamlAbout/About.xmlAbout/PublishedFileId.txtDefs/MainButtons/AICore_DebugLog.xmlINTRODUCTION.txtLanguages/ChineseSimplified/Keyed/RimMind_Core.xmlLanguages/English/Keyed/RimMind_Core.xmlSource/AICoreAPI.csSource/Client/AIRequest.csSource/Client/AIResponse.csSource/Client/IAIClient.csSource/Client/JsonRepairHelper.csSource/Client/OpenAI/OpenAIClient.csSource/Client/OpenAI/OpenAIDto.csSource/Client/Player2/Player2Client.csSource/Client/Player2/Player2Models.csSource/Client/QuotaExceededException.csSource/Client/StructuredTool.csSource/Comps/CompPawnAgent.csSource/Core/AIDebugLog.csSource/Core/AIRequestQueue.csSource/Core/Agent/AgentGoal.csSource/Core/Agent/AgentGoalStack.csSource/Core/Agent/AgentIdentity.csSource/Core/Agent/AgentState.csSource/Core/Agent/AgentStateTransition.csSource/Core/Agent/BehaviorRecord.csSource/Core/Agent/CooldownFilter.csSource/Core/Agent/DedupFilter.csSource/Core/Agent/DefaultAgentProvider.csSource/Core/Agent/GoalGenerator.csSource/Core/Agent/IAgentActionBridge.csSource/Core/Agent/IAgentProvider.csSource/Core/Agent/IPerceptionFilter.csSource/Core/Agent/PawnAgent.csSource/Core/Agent/PerceptionBuffer.csSource/Core/Agent/PerceptionPipeline.csSource/Core/Agent/PriorityFilter.csSource/Core/Agent/RiskLevel.csSource/Core/Agent/StrategyOptimizer.csSource/Core/AgentBus/AgentBus.csSource/Core/AgentBus/AgentBusEvent.csSource/Core/AgentBus/AgentBusEventType.csSource/Core/AgentBus/EventBusAdapter.csSource/Core/AgentBus/Events/ActionEvent.csSource/Core/AgentBus/Events/AgentLifecycleEvent.csSource/Core/AgentBus/Events/DecisionEvent.csSource/Core/AgentBus/Events/GoalEvent.csSource/Core/AgentBus/Events/MemoryEvent.csSource/Core/AgentBus/Events/PerceptionEvent.csSource/Core/AgentBus/IEventBus.csSource/Core/Context/BudgetScheduleResult.csSource/Core/Context/BudgetScheduler.csSource/Core/Context/BudgetSchedulerConfig.csSource/Core/Context/ContextDiff.csSource/Core/Context/ContextEngine.csSource/Core/Context/ContextEntry.csSource/Core/Context/ContextKeyRegistry.csSource/Core/Context/ContextLayer.csSource/Core/Context/ContextRequest.csSource/Core/Context/ContextSnapshot.csSource/Core/Context/EmbedCache.csSource/Core/Context/HistoryEntry.csSource/Core/Context/HistoryGameComponent.csSource/Core/Context/HistoryManager.csSource/Core/Context/KeyMeta.csSource/Core/Context/RelevanceTable.csSource/Core/Context/ScenarioRegistry.csSource/Core/Context/SchemaRegistry.csSource/Core/Context/SemanticEmbedding.csSource/Core/Extensions/IAgentModeProvider.csSource/Core/Extensions/IAudioPlayer.csSource/Core/Extensions/IParameterTuner.csSource/Core/Extensions/ISensorProvider.csSource/Core/Extensions/IStreamingResponseHandler.csSource/Core/Flywheel/EmbeddingSnapshotStore.csSource/Core/Flywheel/FlywheelAnalysisReport.csSource/Core/Flywheel/FlywheelAutoApplyMode.csSource/Core/Flywheel/FlywheelGameComponent.csSource/Core/Flywheel/FlywheelParameterStore.csSource/Core/Flywheel/FlywheelRuleEngine.csSource/Core/Flywheel/FlywheelTelemetryCollector.csSource/Core/GameContextBuilder.csSource/Core/Perception/PerceptionBridge.csSource/Core/Prompt/ContextComposer.csSource/Core/Prompt/PromptBudget.csSource/Core/Prompt/PromptSection.csSource/Core/Prompt/StructuredPromptBuilder.csSource/Core/Prompt/TaskInstructionBuilder.csSource/Debug/AICoreDebugActions.csSource/Npc/HybridStorageDriver.csSource/Npc/IStorageDriver.csSource/Npc/LocalStorageDriver.csSource/Npc/MapNpcComponent.csSource/Npc/NpcManager.csSource/Npc/NpcModels.csSource/Npc/NpcProfileBuilder.csSource/Npc/Patch_MapNpcLifecycle.csSource/Npc/Player2StorageDriver.csSource/Npc/ResponseDispatcher.csSource/Npc/StorageDriverFactory.csSource/Patch/AITogglePatch.csSource/Patch/PerceptionBridge_PatchDowned.csSource/Patch/PerceptionBridge_PatchHealth.csSource/Patch/PerceptionBridge_PatchMentalState.csSource/Patch/PerceptionBridge_PatchMood.csSource/Patch/PerceptionBridge_PatchRaid.csSource/Settings/AICoreSettings.csSource/Settings/ContextSettings.csSource/UI/AICoreSettingsUI.csSource/UI/Window_AIDebugLog.csSource/UI/Window_AgentDialogue.cs
💤 Files with no reviewable changes (1)
- Source/Core/Prompt/StructuredPromptBuilder.cs
| public FlywheelGameComponent() : base() { } | ||
| public FlywheelGameComponent(Game game) : base() { } |
There was a problem hiding this comment.
🧩 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:
- 1: https://rimworldwiki.com/wiki/Modding_Tutorials/GameComponent
- 2: https://github.com/roxxploxx/RimWorldModGuide/wiki/SHORTTUTORIAL:-WorldComponent-and-GameComponent
修复 GameComponent 构造器:必须传递 game 参数给基类。
RimWorld 1.6 要求 GameComponent 子类构造器必须按以下规范实现:
- 仅保留
(Game game)签名的单一构造器(不需要无参构造器) - 必须显式调用
: 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) 并将该参数传递给基类。
| 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!); | ||
| } |
There was a problem hiding this comment.
🧩 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.csRepository: 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 -100Repository: 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 -20Repository: 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).
| 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(); | ||
| } |
There was a problem hiding this comment.
严重: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.
| 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.
There was a problem hiding this comment.
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 viaLongEventHandler.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
⛔ Files ignored due to path filters (3)
AGENTS.mdis excluded by!**/*.mdCLAUDE.mdis excluded by!**/*.mdREADME.mdis excluded by!**/*.md
📒 Files selected for processing (41)
INTRODUCTION.txtLanguages/ChineseSimplified/Keyed/RimMind_Core.xmlLanguages/English/Keyed/RimMind_Core.xmlSource/AICoreAPI.csSource/AICoreMod.csSource/Client/AIResponse.csSource/Client/Player2/Player2Client.csSource/Core/AIDebugLog.csSource/Core/AIRequestQueue.csSource/Core/Agent/PawnAgent.csSource/Core/AgentBus/AgentBus.csSource/Core/Context/BudgetScheduleResult.csSource/Core/Context/BudgetScheduler.csSource/Core/Context/ContextDiff.csSource/Core/Context/ContextEngine.csSource/Core/Context/ContextKeyRegistry.csSource/Core/Context/ContextLayer.csSource/Core/Context/ContextSnapshot.csSource/Core/Context/HistoryGameComponent.csSource/Core/Context/HistoryManager.csSource/Core/Context/ScenarioRegistry.csSource/Core/Context/SchemaRegistry.csSource/Core/Extensions/ISensorProvider.csSource/Core/Extensions/IStreamingResponseHandler.csSource/Core/Extensions/Sensor/AgentToolDefinition.csSource/Core/Flywheel/FlywheelBuiltinTuner.csSource/Core/Flywheel/FlywheelGameComponent.csSource/Core/Flywheel/FlywheelParameterStore.csSource/Core/Flywheel/FlywheelRuleEngine.csSource/Core/Flywheel/FlywheelTelemetryCollector.csSource/Core/JsonTagExtractor.csSource/Core/Sensor/SensorManager.csSource/Debug/AICoreDebugActions.csSource/Npc/HybridStorageDriver.csSource/Npc/IStorageDriver.csSource/Npc/LocalStorageDriver.csSource/Npc/Player2StorageDriver.csSource/Settings/AICoreSettings.csSource/UI/AICoreSettingsUI.csSource/UI/Window_AgentDialogue.csTests/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
| var engine = RimMindAPI.GetContextEngine(); | ||
| var snapshot = engine.BuildSnapshot(request); |
There was a problem hiding this comment.
潜在空引用异常
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.
| 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.
| var engine = RimMindAPI.GetContextEngine(); | ||
| var snapshot = engine.BuildSnapshot(request); |
There was a problem hiding this comment.
潜在空引用异常
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.
| var engine = RimMindAPI.GetContextEngine(); | ||
| var snapshot = engine.BuildSnapshot(request); |
There was a problem hiding this comment.
同上: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.
| 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。
| 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; | ||
| } |
There was a problem hiding this comment.
这些测试在测一份“复制实现”,不是在测 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)并在测试中直接引用该类以替代测试文件内的私有方法。
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
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
Summary
合并 feat/architecture 分支到 main,包含 Core 模块的大规模架构重构与缺陷修复。
Changes
Commits
Checklist
Summary by CodeRabbit
发布说明
新功能
改进
调试/工具