claude-mem 工单分诊 Phase 09:.git 防护、插件禁用状态与陈旧生成器修复实战
本篇以 claude-mem 仓库中 Issue 分诊系列(.maestro/playbooks/2026-02-23-Issue-Triage)的 Phase 09 修复文档为骨架,完整展开四个低优先级但影响面明确的修复:防止 CLAUDE.md 被写入 .git/ 导致仓库引用损坏(#1165)、尊重插件禁用状态(#781)、阻止 UserPromptSubmit 每轮重复注入上下文(#1079)、以及陈旧 AbortController 造成的队列停滞(#1099)。读完后,你将了解每个问题的根因定位方式、一行守卫与三层修复的具体实现,以及配套测试与回归验证的落点,可复用到任何"本地 Hook + 常驻 Worker"架构的 Agent 工具链中。
背景:Phase 09 在分诊体系中的定位
原始文档 TRIAGE-09-Minor-Fixes-And-Cleanup.md 将本阶段定义为"不影响核心功能的低优先级项"。文档开头给出了两个重要的前提判断,体现了务实的分诊策略:
- 本阶段涉及的"安全"问题对 localhost-only 的应用而言大部分是理论风险,因此修复目标是防御性加固而非对抗真实攻击向量——例如
.git/防护被刻意设计为"每个写入点一行守卫",而不是引入一个isInsideGitInternals()工具模块,文档原话是"Focus on the actual write sites, not theoretical attack vectors"; - 项目名冲突(project name collision)问题已在 Phase 03 解决,本阶段不再处理。
Phase 09 最终解决的 Issue 清单为:#1165(.git 损坏)、#781(插件禁用未被尊重)、#923(加载慢)、#1079(上下文重复注入)、#1099(陈旧 AbortController)。其中 #923 在文档中仅列入已解决清单、无独立任务展开;其余四项各有明确的修复任务、实现记录与测试计数。下文按文档的任务顺序逐一展开,并结合当前仓库源码核对实际落点。
修复一:阻止 CLAUDE.md 写入 .git/ 目录(#1165)
问题与修复策略
claude-mem 的核心机制之一是把压缩后的观察记录(observations)写成 CLAUDE.md(或 AGENTS.md)注入到项目目录,供后续会话自动加载。#1165 的问题是:某些路径解析异常时,这些文件可能被写进 .git/ 内部,而 .git/ 下的文件会被 Git 当作 refs/对象数据,直接损坏仓库引用。
文档给出的修复策略极简:搜索所有写入 CLAUDE.md/AGENTS.md 的代码路径,在每个写入点前加一个路径检查——
if (resolvedPath.includes('/.git/') || resolvedPath.includes('\\.git\\')) return;
文档特别强调这是"每写入点一行守卫",并点名了 4 个实际写入点。当前仓库源码与文档记录完全一致,且实际实现比文档中的最小示例更严格——同时覆盖了正斜杠、反斜杠两种分隔符,并追加了 endsWith 判断以拦截以 .git 结尾的路径:
// src/utils/claude-md-utils.ts(L80)
if (resolvedPath.includes('/.git/') || resolvedPath.includes('\\.git\\') || resolvedPath.endsWith('/.git') || resolvedPath.endsWith('\\.git')) return;
四个写入点及其守卫位置:
| 写入函数 | 文件 | 守卫行 |
|---|---|---|
writeClaudeMdToFolder() |
claude-md-utils.ts | L80 |
writeAgentsMd() |
agents-md-utils.ts | L10 |
writeClaudeMdToFolder() |
claude-md-commands.ts | L242 |
writeClaudeMdToFolderForRegenerate() |
regenerate-claude-md.ts | L243 |
以 claude-md-utils.ts 中的 writeClaudeMdToFolder() 为例,守卫位于 path.resolve() 之后、文件操作之前,即任何后续 writeFileSync/renameSync 都不可达 .git/ 路径。同一函数还体现了该写入通道的其他工程细节:通过 .tmp 临时文件 + renameSync 保证原子写入,并通过 toBmpSafe() 过滤 astral 平面字符(避免 Claude Code 截断代理对导致会话损坏)。文档记录该修复新增 4 个测试、提交 2616ac09;仓库中对应测试位于 claude-md-utils.test.ts。
修复二:尊重插件禁用状态(#781)
问题与检查契约
用户在 Claude Code 中禁用 claude-mem 插件后(settings.json 中 enabledPlugins["claude-mem@thedotmack"] = false),插件的 Hook 入口和 Worker 服务应当完全静默。#781 的问题是禁用状态未被尊重,Hook 仍在触发。文档要求的修复契约:
- 在每个 Hook 入口点顶部检查插件是否被禁用;
- 读取
~/.claude/settings.json检查禁用插件列表; - 若被禁用,立即以退出码 0 结束——该检查必须是同步读取 + JSON 解析级别的快速操作,不能阻塞 Hook 链路。
核心实现:isPluginDisabledInClaudeSettings()
文档记录创建了 plugin-state.ts,当前源码与其描述一致,完整实现如下:
// src/shared/plugin-state.ts
const PLUGIN_SETTINGS_KEY = 'claude-mem@thedotmack';
export function isPluginDisabledInClaudeSettings(): boolean {
try {
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
const settingsPath = join(claudeConfigDir, 'settings.json');
if (!existsSync(settingsPath)) return false;
const raw = readFileSync(settingsPath, 'utf-8');
const settings = parseJsonWithBom<Record<string, any>>(raw);
return settings?.enabledPlugins?.[PLUGIN_SETTINGS_KEY] === false;
} catch (error: unknown) {
logger.error('CONFIG', 'Failed to read Claude settings', { ... });
return false;
}
}
从源码可以看出几个值得注意的设计点:
- 配置目录可覆盖:优先取
CLAUDE_CONFIG_DIR环境变量,回退到~/.claude,便于多实例/测试场景隔离; - 容错解析:通过
parseJsonWithBom(见 atomic-json.ts)处理带 BOM 的 JSON,避免 Windows 下手工编辑 settings.json 时解析失败; - 失败即启用(fail-open):settings.json 不存在、JSON 解析异常或任何意外错误都返回
false(视为未禁用),保证禁用检查本身永远不会把正常用户挡在门外。
三个提前退出点
文档记录该检查被加入三个入口,当前仓库均可核对到:
worker-service.ts:main()——worker-service.ts 中,仅当命令为undefined或属于 Hook 触发类命令(start/hook/restart/daemon)时才执行禁用检查,被禁用则提前退出,避免 Hook 拉起整个 Worker;bun-runner.js——bun-runner.js 在拉起 Bun 进程之前做同样的检查。该脚本内嵌了一份独立实现(不依赖 TS 模块),这是因为它运行在 Worker 启动链路的最前端,必须零依赖、零启动成本。测试 bun-runner.test.ts 会静态断言脚本源码中包含isPluginDisabledInClaudeSettings()函数定义;smart-install.js——安装器在依赖检查之前同样先做禁用检查(按分诊文档记录),避免被禁用用户仍触发依赖安装。
文档记录该修复新增 7 个测试、全量 1070/1070 通过,提交 d6bc4495;仓库中的 plugin-disabled-check.test.ts 覆盖了 settings.json 不存在、enabledPlugins 缺失、键值为 true/非 false 等分支,均断言"未禁用",仅在显式为 false 时断言"已禁用"。
修复三:UserPromptSubmit 不再每轮重复注入上下文(#1079)
问题机理
claude-mem 的 UserPromptSubmit Hook 会在每次用户提交 prompt 时向 Worker 的 /api/sessions/init 发请求。#1079 的缺陷是:这个 Hook 在每一轮(而非仅首轮)都会触发完整的上下文注入与会话初始化,造成重复开销与上下文噪声。
文档要求的修复:通过 Worker API 引入会话级标志——一旦某会话已完成上下文注入,后续轮次跳过重复注入;注入前先查询 Worker 的会话状态。
实现:contextInjected 标志
当前源码与文档记录一致。Worker 侧在 /api/sessions/init 响应中计算并返回该标志,见 SessionRoutes.ts:
const contextInjected = this.sessionManager.getSession(sessionDbId) !== undefined;
即:SessionManager 内存中已存在该会话的 SDK Agent 状态,说明注入已完成。Hook 侧的 session-init.ts 接收该响应字段:
logger.debug('HOOK', 'session-init: Received from /api/sessions/init',
{ sessionDbId, promptNumber, skipped: initResult.skipped, contextInjected: initResult.contextInjected });
行为边界被文档明确限定,值得强调:当 contextInjected=true 时跳过的是 POST /sessions/{sessionDbId}/init(SDK Agent 的重复初始化),而 prompt 追踪(/api/sessions/init 调用本身)仍然每轮执行——这样既消除了重复注入,又不破坏逐轮 prompt 记录。文档记录新增 6 个测试、1076/1076 通过。
修复四:陈旧 AbortController 队列停滞(#1099)
问题机理
Worker 为每个会话维护一个"生成器"(generator,负责驱动 SDK Agent 消费队列消息)。若生成器的 AbortController 处于陈旧/挂起状态,await 它的 Promise 会无限等待,整个会话队列停滞。#1099 正是这一症状。文档要求:搜索全部 AbortController 用法、为等待加 AbortSignal.timeout(30000)、超时后重置生成器状态。
三层修复
文档将其总结为"Three-layer fix",当前仓库源码可逐层对应:
第 1 层:活动心跳时间戳 lastGeneratorActivity。 该字段定义在 worker-types.ts 的 ActiveSession 类型上,并在 SessionManager.ts 创建会话时初始化。三处刷新点与文档描述一一对应:
processAgentResponse()——所有 Agent 产生响应时刷新,见 ResponseProcessor.ts;getMessageIterator()——队列每次 yield 消息时刷新,见 SessionRoutes.ts;- 生成器启动时刷新,见 SessionManager.ts。
第 2 层:ensureGeneratorRunning() 中的陈旧检测。 SessionRoutes.ts 的 ensureGeneratorRunning() 是所有生成器启动的统一入口(init、summarize、overflow-recycle 等均经此)。修复逻辑为:若 lastGeneratorActivity 距今超过 30 秒阈值,说明现有 controller 已陈旧——中止它、重置生成器状态、启动一个全新的生成器,而不是继续在死状态上等待。
第 3 层:deleteSession() 的超时兜底。 SessionManager.ts 中删除会话时 await 卡死的生成器 Promise 不再无限挂起:
AbortSignal.timeout(30_000).addEventListener('abort', () => resolve(), { once: true });
即用一个 30 秒的超时信号给 Promise 加"逃逸出口"。测试侧对应 stale-abort-controller-guard.test.ts。文档记录该修复新增 10 个测试,958/958 通过(另有 24 个与本变更无关的既有失败)。
回归验证与遗留失败
Phase 09 的最后一项任务是"运行 npm test 并修复所有失败"。文档给出的结论:非既有测试全部通过;24 个既有失败集中在无关领域(需要真实服务的集成测试、ChromaSync、MarkdownFormatter 的 MCP 文本变化、openclaw、以及 transcripts/cli.ts 的日志规范检查),并明确"no regressions from #1099 fix"。这一阶段也示范了分诊文档的标准收尾方式:用失败清单的归因而非单纯计数来证明无回归。
小结:低优先级修复的工程标准
对照 Phase 09 的四项修复,可以提炼出该仓库处理"低优先级但影响明确"问题的通用方法:
- 最小侵入:#1165 用一行守卫 × 4 个实际写入点解决,拒绝抽象出防御理论攻击向量的工具模块;
- 快速检查放在链路最前端:#781 的禁用检查是同步读文件级别,且复制进
bun-runner.js使其在 Bun 启动前就能生效; - 状态驱动而非事件驱动的去重:#1079 用 Worker 内存会话状态(
getSession()是否存在)作为contextInjected的事实来源,Hook 侧保持无状态; - 多层防御处理挂起:#1099 同时提供心跳检测、入口重置、
AbortSignal.timeout兜底,任何一层生效都能解除停滞。
相关文档与源码入口:TRIAGE-09 分诊文档、plugin-state.ts、claude-md-utils.ts、agents-md-utils.ts、claude-md-commands.ts、regenerate-claude-md.ts、session-init.ts、SessionManager.ts、SessionRoutes.ts,以及测试 plugin-disabled-check.test.ts、bun-runner.test.ts、claude-md-utils.test.ts、stale-abort-controller-guard.test.ts。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00