AI 代理工作区中自动生成 state 文件的排查记录

背景 某 AI 代理工具的工作区目录中,反复出现一个状态文件。即使手动删除,过一段时间后文件仍会重新生成。 工作区中同时还能看到另一个隐藏目录下的状态文件。经过人工对比,这两个文件内容完全相同。于是问题变成了: 现象 工作区中存在两个状态文件: 其中顶层的 state 文件删除后会再次出现。隐藏目录中的 state 文件一直存在。 两个文件内容相同,JSON 结构也相同,主要记录工作区初始化状态,例如: 这些字段看起来不像用户数据,也不像配置密钥,而是程序用于判断工作区是否完成初始化的运行状态信息。 排查方式 排查过程中保持只读原则,没有删除、移动、修改文件,也没有重启服务。 主要检查方向包括: 排查结果 结果显示,这两个文件都属于工具正常运行会产生的文件,但地位并不完全相同。 顶层的状态文件是当前版本代码中的 canonical 路径,也就是当前主状态文件。 隐藏目录中的状态文件是 legacy 兼容路径,也就是旧路径或兼容路径。当前代码仍然会读取它,并在需要时把里面的状态迁移回顶层的 canonical 文件。 换句话说,这不是两个互相冲突的状态文件,而是同一份工作区状态在新旧路径之间共存。 为什么删除后还会出现 删除顶层状态文件后,如果隐藏目录中的 legacy 状态文件仍然存在,那么下一次工具启动、工作区初始化、代理会话创建或 heartbeat 触发时,程序会重新读取 legacy 状态。 如果程序发现顶层 canonical 状态文件不存在,就会根据 legacy 状态重新生成顶层文件。 因此,文件并不是“神秘复活”,而是程序的兼容迁移逻辑主动恢复了它。 可以理解为: 是否属于异常 从排查结果看,没有明显异常迹象。 两个文件内容一致,权限正常,字段结构正常,也没有发现异常进程或可疑写入行为。顶层文件比隐藏目录中的文件更新,说明当前程序确实更偏向使用顶层 canonical 文件,而隐藏目录中的文件更像历史兼容残留。 这种情况更像是软件版本迁移期间的兼容设计,而不是数据损坏或异常垃圾文件。 …

使用 systemd 为 AI 网关服务设置内存上限:从高峰占用到 6GB 硬限制

背景 某台小型 Linux 主机上运行着一个 AI 代理网关服务。该服务平时占用并不高,但在运行一段时间后,systemctl status 显示历史内存峰值曾达到约 5.5GB。 状态中可以看到类似结构: 这说明服务并不是单一进程,而是由主进程和若干子进程共同组成。若只限制某个 Node 进程,无法可靠覆盖整个服务组。更合理的方式是使用 systemd cgroup 资源限制,对整个 service 统一设置内存上限。 问题 该主机总内存约为 8GB,且没有启用 swap。AI 网关服务如果在高负载或异常情况下继续增长内存占用,可能导致系统进入异常状态,例如: 因此,需要给该服务设置一个最大内存使用上限。 目标不是修改应用源码,也不是给应用打补丁,而是通过系统服务管理器限制资源使用。 方案选择 最终采用的是 systemd 用户级 service drop-in 配置。 这种方式的特点是: 也就是说,这不是“打补丁”,而是系统层面的标准资源控制。 配置目标 最终设置为: 含义如下: 启用内存统计,让 systemd 记录该服务的内存使用情况。 软限制。服务内存超过 5GB 后,systemd 会开始施压、回收、限速,尽量避免继续膨胀。 硬限制。整个服务 cgroup 的内存占用不能超过 6GB。主进程和子进程合计计算。 如果服务因超出内存限制触发 OOM,systemd …

一次本地 AI 代理 HEARTBEAT 频率与新闻摘要格式的调整记录

背景 某本地 AI 代理系统已经开始定期执行 HEARTBEAT 任务,并能够通过邮件发送状态报告。整体执行链路已经跑通:任务可以被触发,邮件可以正常发出,收件端也能收到报告。 不过在实际使用后,暴露出两个需要调整的问题: 第一,HEARTBEAT 执行频率过高。默认频率约为 30 分钟一次,对于日常状态汇总来说过于密集,容易造成邮件噪音。 第二,新闻摘要任务的输出质量不理想。虽然系统能够抓取公开 RSS 新闻标题并生成邮件,但内容基本停留在“标题列表 + 固定占位摘要”的层面,缺少真正可读的摘要信息。 因此,这次调整主要围绕两个目标展开: 问题一:新闻摘要内容过于粗糙 原先的新闻邮件结构大致如下: 这种输出存在明显问题: 期望的格式是每条新闻形成一个小型三语新闻卡片,例如: 这种格式的优点是: 新闻摘要任务规则的修改 原来的任务说明只要求“发送一封简洁的中文新闻摘要邮件”,这会导致 AI 代理自然倾向于输出中文简报,而不是三语新闻卡片。因此,任务规则需要明确改写。 修改后的规则重点包括: 修改后的核心目标不是“多翻译几种语言”,而是让邮件本身变成可读、可用、可追溯的国际新闻摘要。 问题二:HEARTBEAT 默认频率过高 另一个问题是 HEARTBEAT 默认频率较高。系统状态检查本身已经正常执行,但 30 分钟一次对于当前用途来说没有必要。 经过只读检查后,确认当前系统并不是通过外部定时器或 cron 控制 HEARTBEAT,而是由 AI 代理自身的配置项控制。 检查结果显示: 因此,最终修改目标是: 修改方式 修改前先进行了 dry-run,确认配置写入动作可以被接受。 示意命令如下: dry-run 成功后,再实际写入: 系统返回配置已更新,但同时提示需要重启 …

用 Heartbeat 文件管理本地 AI 代理的每日邮件通知

背景 在本地部署 AI 代理之后,一个很自然的需求是让它承担一些低风险、可重复的日常检查工作。例如: 这些任务看起来都可以用传统脚本完成,例如写一个 Python 脚本,再配合 cron 或 systemd timer 定时执行。但如果本地 AI 代理本身已经内置了 Heartbeat 机制,那么更合理的做法不是再造一套外部自动化系统,而是把任务边界写进代理的 Heartbeat 配置文件,让代理自己按授权范围执行。 本文记录一次围绕 Heartbeat 文件、邮件通知和发件人显示名的整理过程。 Heartbeat 文件的定位 最初容易产生一个误解:Heartbeat 文件是不是一份普通说明文档? 实际检查后可以确认,它更适合被理解为: 本地 AI 代理的周期任务授权清单。 它不是长期记忆文件,也不是系统状态总表,更不是脚本。它的核心作用是告诉代理: 因此,Heartbeat 文件不应存放大量历史信息、主机清单、连接细节、密码、令牌、日志原文或临时排障内容。那些信息应该分别放在长期记忆文件、工具环境说明文件、每日记录文件或专门的配置文件中。 Heartbeat 文件只负责回答一个问题: 代理现在被授权定期做什么? 最初的需求 本次需求可以分成四类每日邮件: 要求是: 这样的需求非常适合写进 Heartbeat 文件。 不应该额外写一套完整脚本 一开始可以想到一种传统方案: 这套方案当然可行,但它有一个问题: 如果脚本自己完成全部工作,那么即使没有 AI 代理,也能收到邮件。 这就偏离了 Heartbeat 文件的真正用途。此时 …

一次 OpenClaw Gateway 迁移后的安全自检与问题收尾记录

背景 某台小型 Linux 主机上运行着 OpenClaw Gateway,作为个人运维辅助入口使用。该实例经历过一次迁移和配置调整,迁移后需要确认几件事: 本次排查的原则是只读优先:先通过 systemd、journal、监听端口、配置目录和进程状态确认现状,不直接修改、不删除、不重启。只有当证据明确显示配置变更需要重启生效,且重启本身也能释放内存压力时,才执行服务重启。 初始自检结论 自检首先确认了几个基础事实: 这说明迁移后的实例基本可用,不属于“服务没起来”或“运行了错误实例”的情况。 不过自检也发现了几个需要继续收尾的问题: 这些问题并不等于实例不可用,但说明迁移后仍需要做一轮细化确认。 问题一:trusted proxy warning 的真正含义 日志中最容易误判的一条 warning 是: 表面看,这像是某个内网地址访问了 OpenClaw,并携带了代理头。进一步查看完整上下文后,发现实际情况并不是“路由器直接访问了 Gateway”。 真正的连接关系是: 日志中的关键信息可以分成两类: 其中: 因此,应该加入 trustedProxies 的不是路由器地址,而是本机 loopback 地址。 正确方向是: 不应在证据不足的情况下把路由器地址加入 trusted proxy。因为当前证据只说明路由器地址出现在 forwarded header 中,并不是它直接连接 OpenClaw Gateway。 问题二:配置变更需要重启才能生效 设置 gateway.trustedProxies 后,日志出现了新的提示: 这说明 OpenClaw 已检测到配置变化,但该项不能热重载,必须重启 Gateway 才能生效。 这里有一个重要判断: …

把操作经验写入 TOOLS.md:AI 代理长期规则如何固化

本地 AI 代理系统真正进入长期使用阶段后,最重要的问题不再只是“它能不能完成任务”,而是“它以后会不会按照稳定、可预期、边界清楚的方式完成任务”。 一次迁移、一次修复、一次浏览器调用、一次误操作事故,如果只停留在当前对话里,很快就会消失。下一次再遇到类似场景,代理可能重新犯同样的错误。 因此,迁移完成后的一个重要收尾工作,是把经验写入长期规则文件。 在这个过程中,TOOLS.md 成为最关键的文件之一。 为什么需要长期规则 AI 代理和普通脚本不同。 普通脚本只会执行写死的逻辑。AI 代理则会根据自然语言指令、上下文、工具可用性和自己的推断来决定下一步操作。 这带来灵活性,也带来风险。 如果只告诉代理: 它可能会自己推断出很多没有被允许的做法: 这些推断未必是恶意的,但可能与实际使用场景冲突。 因此,长期规则的作用不是单纯记录“怎么做”,还要记录: 对长期运行的 AI 代理来说,负面边界和正向流程同样重要。 日期记忆不适合保存持续规则 很多代理工作区会有按日期排列的记忆文件,例如: 这类文件适合记录当天发生了什么: 但它们不适合作为长期规则的唯一来源。 原因是日期文件本质上是事件记录,而不是行为规范。代理以后要决定如何调用工具时,不一定会优先读取某一天的历史记录。 例如: 这条记录能说明事情发生过,但不能保证代理以后每次用浏览器都遵守这些规则。 因此,持续规则应该写入专门的规则文件。 为什么浏览器规则应该写入 TOOLS.md 在 OpenClaw 工作区中,不同 Markdown 文件承担不同职责。 抽象上可以这样划分: 远程浏览器/CDP 能力本质上是一个工具。它不是用户偏好,也不是人格设定,也不是普通历史记忆。 它包含: 所以它应该写入 TOOLS.md。 这能让代理在以后调用浏览器时,把它当作一个受约束的工具,而不是凭当前对话临时推断。 工具规则不只是命令 一个常见误区是把工具规则写成几条命令。 例如: 这还不够。 真正可长期使用的工具规则应该包含五个层次: 以远程浏览器为例,资源定义应包括: 使用前检查应包括: 正常启动流程应包括: …

迁移后的文件清理:用 sha256 审计安全删除 67325 个重复文件

主实例迁移完成后,旧主机上往往会留下大量重复文件。 这些文件看起来像“迁移残留”,但不能直接删除。因为旧主机上可能同时保留着几类不同性质的数据: 如果不加区分地删除旧工作区,很容易误删尚未迁移的项目、迁移后仍需要的扩展资源,或者旧主机上独有的备份文件。 因此,迁移后的文件清理不能靠“看目录名猜”,而应该靠审计结果决定。 本篇记录的是一次通过相对路径、文件大小和 sha256 hash 对比,安全删除旧主机重复文件的过程。 清理前的基本局面 迁移完成后,系统已经形成新的主从结构: 旧主机上的大部分工作区文件已经迁移到新主机,但不是全部。 其中有一个大型项目目录因为新主机存储容量不足,暂时继续保留在旧主机上,作为扩展项目使用。另一个浏览器/CDP 目录也属于外部工具资源,不属于本次工作区清理范围。 因此,清理前必须先确定排除范围。 不能清理的目录 迁移后文件清理首先要明确两类禁止触碰目录。 第一类是大型项目目录。 它因为容量原因没有迁移到新主机,仍然需要继续保留在旧主机上。如果清理脚本把它当成普通迁移残留,就会造成严重误删。 可以抽象表示为: 第二类是浏览器/CDP 专用目录。 这个目录用于远程浏览器能力,属于旧主机作为扩展资源主机的一部分,不是旧 OpenClaw 主实例残留。 可以抽象表示为: 因此,清理审计范围应该是: 这个边界非常重要。否则“清理旧工作区”很容易被误解成“删除旧主机上所有 OpenClaw 相关目录”。 为什么不能直接用 rsync –delete 迁移清理中,rsync –delete 看起来很诱人。它可以让两个目录快速变成一致状态。 但在这个场景中,它并不适合。 原因是旧主机上故意保留了一些新主机没有的内容。如果直接用 rsync –delete 或类似逻辑,就可能把旧主机独有但仍然需要的文件删除。 尤其是以下几类文件: 因此,本次清理不追求两个工作区完全一致,而是只删除那些已经被确认“新主机上有同名文件,且内容完全相同”的文件。 这比 rsync –delete 更保守,也更适合迁移后的分层清理。 审计方法:相对路径、大小和 sha256 安全清理的核心是生成两边文件清单。 对旧主机工作区生成 …

AI 代理误杀普通浏览器的事故复盘:权限边界必须写死

在本地 AI 代理系统中,浏览器自动化是一项非常有用的能力。代理可以打开网页、搜索信息、读取页面、协助登录后台、处理表单,甚至通过 CDP 控制完整浏览器。 但浏览器自动化也有一个明显风险:如果代理没有严格区分“自己可管理的浏览器”和“用户正在使用的普通浏览器”,它就可能误伤用户的真实桌面环境。 本篇记录的是一次典型的边界事故:AI 代理本来只应该打开和关闭自己的专用浏览器,却因为使用了过宽的进程匹配,把用户正常使用的普通 Brave 浏览器也关闭了。 这不是单纯的命令错误,而是一次权限边界设计错误。 背景:主实例和浏览器分离 迁移后的结构中,OpenClaw 主实例运行在常驻主机上,桌面浏览器运行在另一台桌面主机上。 浏览器调用链路大致是: 为了隔离用户日常浏览器和 AI 代理浏览器,桌面主机上准备了一个专用 profile: OpenClaw 只能使用这个专用 profile。用户日常使用的普通 Brave profile、普通窗口、普通标签页和普通会话,都不属于 OpenClaw 的管理范围。 从设计上看,这个边界是清楚的。 但事故说明:只靠“人知道边界”不够,必须让 AI 代理的规则和命令也严格遵守这个边界。 事故经过 最初的任务很简单:让 AI 代理在桌面主机上远程打开一个可见 Brave 窗口,用于测试浏览器/CDP 能力。 代理确实打开了一个新的可见窗口。 但在打开之前,它先关闭了桌面主机上原本已经打开的普通 Brave 窗口。 随后,又让代理关闭刚才打开的浏览器窗口。结果它在关闭时,再次把用户后来重新打开的普通 Brave 窗口关闭了。 也就是说,事故不是一次偶发误操作,而是在两个阶段都发生了边界错误: 这说明代理没有把“OpenClaw 专用浏览器”和“用户普通浏览器”区分开。 真正的问题:它管理了 Brave 进程整体 …

Saturn 如何调用 Mars 浏览器:SSH Tunnel、Brave CDP 与可见窗口

把 OpenClaw 主实例迁移到树莓派之后,一个问题很快出现:主实例已经在树莓派上运行,但完整桌面浏览器仍然在桌面 Linux 主机上。 树莓派适合承担长期在线的 OpenClaw gateway、消息通道和主工作区,但它并不适合作为完整图形浏览器主机。桌面机已经具备图形环境、浏览器、用户会话和更强的桌面交互能力,因此更适合继续提供浏览器/CDP 能力。 这就形成了一种分离式结构: 本篇记录的是:如何让运行在树莓派上的 OpenClaw,按需调用桌面机上的 Brave 浏览器。 为什么不直接在树莓派上运行浏览器 理论上,可以在树莓派上安装 Chromium 或 Brave,再让 OpenClaw 直接使用本机浏览器。但实际并不一定合适。 原因包括: 对一个长期运行的 AI 代理系统来说,主实例和浏览器并不一定要在同一台机器上。 更合理的结构是: 这样,树莓派保持轻量稳定,桌面机只在需要时提供浏览器能力。 CDP 是什么角色 CDP,即 Chrome DevTools Protocol,是 Chromium 系浏览器提供的远程调试和自动化接口。Brave 基于 Chromium,因此也可以通过 CDP 被自动化工具连接。 浏览器启动时指定 remote debugging 参数,例如: 浏览器就会在本机提供一个 CDP endpoint。访问: 如果返回包含 webSocketDebuggerUrl 的 JSON,就说明 …

清理旧 OpenClaw 实例:安装目录、systemd 服务和 shell 残留

OpenClaw 主实例迁移到新主机后,旧主机上的清理工作同样重要。 很多迁移事故并不是发生在复制文件时,而是发生在迁移之后:旧服务还在、旧命令还能执行、旧配置目录仍然存在、shell 里还保留 alias 或 PATH。表面上看主实例已经迁走,实际上旧主机仍然残留一个“半可用”的 OpenClaw 实例。 这种状态非常危险。因为一旦旧实例被误启动,就可能出现两个 OpenClaw 同时存在的情况: 清理旧实例的目标,不是简单删除几个目录,而是让旧主机彻底退出 OpenClaw 主运行链路。 旧实例清理的核心原则 迁移完成后,系统里应该只有一个 OpenClaw 主实例。 旧主机可以继续作为扩展资源主机,例如提供浏览器、图形环境、大型项目目录或临时计算能力,但它不应该再保留可启动的 OpenClaw gateway,也不应该再保留容易误用的 OpenClaw 命令入口。 清理的目标可以概括为: 最重要的是,不要让旧主机继续具备“像主实例一样启动 OpenClaw”的能力。 区分安装目录和状态目录 清理旧实例时,第一个容易混淆的问题是:OpenClaw 的安装目录和状态目录不是一回事。 常见结构大致如下: 软件安装目录通常包含 OpenClaw 程序本体、Node runtime、工具链、dist 文件等。状态目录则包含配置、session、插件状态、浏览器 profile、缓存、认证引用等内容。工作区则保存用户交互产生的文件、memory、projects、output、plans 等。 如果只删除状态目录,而保留安装目录和命令入口,旧实例仍然可能被启动。 如果只删除安装目录,而保留状态目录,旧配置、旧 session、旧浏览器 profile 和旧缓存仍然会留在系统里。 如果只清理 workspace,而不清理 systemd 服务,旧服务可能在某次登录或重启后继续尝试启动,并产生新的错误日志。 因此,清理旧实例必须同时检查三类内容: 停止旧 systemd …