|
|
7f43a6bfc7
|
docs: 重写 README,中英文对齐,历史移入 CHANGELOG
## 问题:README 像架构文档,不像 README
原文 420 行,开头第一句是「以核心域与应用域分离为设计原则的 Agent 框架」。
读者点进来想知道「这东西能给我干什么」,得到的是一句需要三分钟才能理解的定义。
量化一下 AI 味的几个典型指纹:
| 指标 | 改前 | 改后 |
|---|---|---|
| 加粗标记 | **230** 处 | 56 处 |
| 破折号插入语 | **17** 处 | 0 |
| 行数 | 420 | 204 |
| 「项目状态」占比 | 86 行(21%) | 移出 |
几乎每条 bullet 都是同一个模板:粗体术语开头 + 三行解释 + 收尾总结。
结构过度对称、每段都自我收尾,是 AI 写作最明显的痕迹。
## 改法:先说人话,再讲设计
开头四句:
> 一个能自己跑几个月、还记得住事的 Agent 内核。
> 说人话:你给它接上 QQ、WebUI、命令行、文件、浏览器,它就能在这些地方替你干活。
> 白天聊过的事,晚上还记得。三周前说过的偏好,不会因为上下文窗口滚动就丢了。
> 插件崩了它不倒,模型换了它不改。
然后把 `## 设计要点` 改成 `## 它跟别的 Agent 框架哪里不一样` ——从「我们有什么设计」
改成「你和别人有什么不同」。每条都给了**为什么这么选**,不是**我们做得多好**:
- 插件是独立进程 → 因为 `.so` 那套踩过 `dlclose` 空操作、cgo 超时杀不掉线程
- 内核不碰 IO → 好处不是干净,是能换掉整个交互层而不改内核
- 记忆会下沉 → 所以能跑几个月而不把历史全塞进 prompt
`## 能力` 不列 API,列「你能让它干什么」:说话的地方 / 看和听 / 动手 / 记住 / 调度。
## 删掉 227 行重复与历史
- **`## 架构图`(142 行)**:`ARCHITECTURE.md` 里已有完整的「消息处理流程」「7 阶段
钩子」「三层记忆」,README 里这套是**逐字重复**。README 留一张 8 行流程图 +
三段话讲清机制,细节指向架构文档。
- **`## 项目状态`(85 行)**:这是更新日志,不是 README。移入新建的
`CHANGELOG.md` / `CHANGELOG_EN.md`。
## 中英文同步
`README_EN.md` 之前停在旧版式(439 行,含 `Architecture Diagrams` /
`Project Status`),与中文版结构完全不同 —— 不是这次才不同步,是**一直是两份不同的文档**。
本次按节 1:1 对齐(9 节完全一致),英文不是逐字直译,按英文习惯重写:
```
## 它跟别的 Agent 框架哪里不一样 → ## What makes it different
## 能力 → ## What it can do
## 怎么工作的 → ## How it works
## 快速体验 → ## Quick Start
## 代码结构 → ## Code Structure
...
```
补上英文版一直**缺失**的 `## Quick Start` 整节(中文有、英文没有)。
## 顺带修正的事实错误
- 「内置 13 个插件」→ 实际 18 个插件目录(`webui`/`cli`/`files`/`cmd`/`mcp`/`kbtree`/`seq`/`skillmgr` …)
- 「参考 `example/` 目录下的 Go 和 Lua 示例」→ 26 个插件已迁到
[github.com/HologramAgent](https://github.com/HologramAgent),`example/` 只剩 2 个
## 验证
- 两版死链检查:`README.md` / `README_EN.md` 内部链接全部有效
- 9 个二级标题逐一对齐
- 破折号插入语归零(英文剩余 4 处全是列表分隔符)
|
2026-10-31 11:08:12 +08:00 |
|
|
|
acaf0887c7
|
fix(webui): 聊天图片"加载成功却显示加载失败"——HTML 属性里的反斜杠转义
## 缺陷
生产截图:图片加载成功了(img 元素在、src 正确),界面却多显示一行
蓝字「图片加载失败"/>」。
根因是内联事件属性里用了反斜杠转义:
onerror="this.parentElement.innerHTML='<span class=\"att-err\">图片加载失败</span>'"
**HTML 属性值里没有反斜杠转义** —— 那个 \" 就是引号本身,属性在
`<span class=` 之后就闭合了。浏览器切出来的是:
onerror = "this.parentElement.innerHTML='<span class="
剩下的 `att-err">图片加载失败</span>'"/>` 变成**文本节点**直接显示。
症状极具误导性:看起来像"图片坏了",实际图片是好的;而那个 onerror
因为语法错误**永不执行** —— 真失败时反而什么都不显示。
## 修法
不用内联属性(那要求以后每次拼 HTML 都记得转义规则),改成
**捕获阶段的事件委托**:
- error 事件**不冒泡**,只能在捕获阶段拿到(注释里写明了)
- 用 createElement + textContent 构造占位,不再拼 innerHTML 字符串
- 只处理 .chat-attachment-img,不影响正文里的 markdown 图片
## 顺带:两个类没有 CSS
`chat-attachment-img` 与 `att-err` 只出现在 JS 里,CSS 里一条都没有。
图片靠内联 style 撑着还能看,失败占位则完全没有样式。已补。
## 判据(2 条,变异验证)
- `TestChatAttachmentImgHasNoBrokenInlineHandler`:不许内联 onerror、
必须用 DOM API、必须有 error 监听
- `TestDashboardNoEscapedQuoteInHTMLAttributes`:通用扫描,
任何 `name="...\"...` 形态都会红(下次别人写内联事件也拦得住)
变异:把内联 onerror 放回去 ⇒ 两条同时红。
★ 写判据时踩到的坑(都已修):判据被**自己的注释**打红(注释里必须写出
错误写法来解释),需先用已有的 stripJSComments 剥离;另外我先自己写了一个
stripJSComments,与 dashboard_js_scope_test.go 的重名 —— 应复用已有的。
## 顺带修一条脆弱判据
`TestRTChild_RapidInputCollapsesToLatest` 全量跑时偶发红
("连发 50 条却跑了 5 轮")。
探针查实:连发 50 条只花 ~80µs,而测试 provider 的 ChatStream **同步瞬间
返回**,两者同量级 ⇒ 消费方能在连发过程中抢到几次执行。探针显示
dropped=48,collapse 明明生效了。
所以 `Rounds > 3` 是在测调度抖动,不是测契约。改为断言**覆盖主导**
(dropped >= burst/2)。真实通话每轮几十毫秒,连发微秒级,生产里
"轮次≈1" 自然成立。
变异验证:让 dropped 不计数 ⇒ 判据红(dropped=0(连发 50 条))。
go build/vet/gofmt 干净;go test ./... -count=1 -race = 62 ok / 0 FAIL / 0 RACE
|
2026-10-31 10:03:43 +08:00 |
|
|
|
843563120b
|
merge: 安全策略收敛到 security 模块 + 文档全量校订
用户裁定驱动的两项架构修正,以及随之而来的文档体系重整。
## 一、安全策略收敛(3 个提交)
### 1d007e1 工具策略闸真的接上了
生产日志实证 691 个文件里 `denied by policy` 命中 0 次。
根因不是「buildAgent 少传一行」,而是 `kernelmod/agent` 的
`Requires()` 没声明 `FacSecurity` —— 依赖图没理由把 security
的产物交给 agent(只有声明了的 device 拿到了)。
同时发现子 agent(resident/rtchild)各自 New 时都没传 Security,
从构造起就是 nil。
### e78a38b 父为子声明工具策略
修正上一版「子继承父权限」的错误模型。用户裁定:应当是父 agent
为子 agent 声明策略。用的是树里早就有的 `CheckToolFor` /
`PrincipalPolicy` —— 机制完整但零生产调用方。
### 5194a96 安全策略全面收敛到 security 模块
security 是外围内核到核心内核之间的防火墙,自己提供接口、
判定全部内聚在自己内部。调用方只提供事实(谁在跑、有没有父、
原始载荷),不提供结论(我是哪类主体、该走哪条判定链)。
### 8293282 每 agent 一个策略实例
用户裁定「每个 agent 一个,分为 sessionProfile 和 longtimeProfile」。
切分原则是共享注册表 + per-agent 执行视图 —— 整份复制 Runtime
会让插件往 A 声明的档位对 B 不可见。优先级 session > longtime,
反过来就是提权。
## 二、文档体系重整(4 个提交)
### b9b5ac0 建立 future-plan 作为唯一未做登记处
此前「未做」散落各文档,时间一长就与代码脱节。
### cf14b84 / 4963d70 补齐状态分类 + 逐篇核实
32 篇文档此前混着三种时态。逐篇对代码核实,修正 6 处与代码不符
的自述(最危险的是 prod-migration-checklist 写着「已停机」,
那是一次性迁移当场的状态)。
### a402bfd 删除 7 份已完结的执行计划
执行计划在实施完结后变成误导性读物。删前提取三类内容:
教训 → 新建 engineering-lessons.md;
实测数据 → 并入设计文档附录 A;
未完成项 → future-plan.md。
保留 legacy-table-retirement.md 与 interrupt-subsystem-split-plan.md
(被生产代码注释引用,是依据而非计划)。
## 门禁
- go build ./... ✓
- go vet ./... ✓
- go test ./... -count=1:10 轮全绿 ✓
- -race(agent/core + security + kernelmod/all)✓
- SDK 接口冻结检查:third_party/homeagent-sdk/sdk/ diff 为空 ✓
|
2026-10-31 09:19:24 +08:00 |
|
|
|
a402bfd777
|
docs: 删除 7 份已完结的执行计划,把教训与数据提出来
## 为什么删
执行计划在实施完结后**会变成误导性读物**:它记录的是"当时打算做什么",
而读者会读成"现在是什么"。
这在本仓已经造成真实误读 —— `prod-migration-checklist.md` 写着
「状态:HologramAgent 已停机」,那是一次性迁移(2026-10-04)当场的状态,
但照着读会以为服务现在还停着。
## 删了 7 份,删前提取三类内容
| 删掉的文档 | 行数 |
|---|---|
| `memory-restructure-plan.md` | 168 |
| `memory-blockflow-repair-plan.md` | 450 |
| `memory-usability-plan.md` | 309 |
| `scene-memory-fix-plan.md` | 271 |
| `toolcall-parallel-execution-plan.md` | 578 |
| `prod-migration-checklist.md` | 157 |
| `entities-retirement-plan.md` | 118 |
**提取去向**:
1. **教训 → 新建 `engineering-lessons.md`**
这些是它们唯一还有长期价值的部分。汇总成一篇,并按形态归类:
- 判据类(5 条):结构判据不能替代质量判据 / 测「接线之后」≠测「有没有接线」/
验证通过≠架构正确 / fixture 照臆测字段编 / 先改代码再写测试
- 接线类(3 条):机制≠接线 / 依赖图是唯一保证顺序的地方 / 单数改了复数漏了
- 权限与生命周期(3 条):「接上了」有三层 / 以调用方名字为键的表要清理 /
提权口子:让调用方声明自己是谁
- 流程类(3 条):长任务纪律 / 数字要现场取 / 计划不进仓库
文末点出它们共同的形态:**某种"看起来对了"的信号与"真的对了"之间存在缺口。**
2. **实测数据 → 并入 `toolcall-contract-and-sequence-design.md` 附录 A**
那是新旧内核的对比压测(288 条输入 × 两版),核心结论值得留:
```
场景 旧版中位/工具数 新版中位/工具数
slowbatch8 0.220s / 0 个 0.409s / 8 个
```
★ **旧版"更快"是假的** —— 它的 `openai.lua` 缺 `stream_index` 透传,
多个分片并到槽 0 ⇒ 每个工具报参数错误而一个都没真跑。
这是「只看耗时会被骗」的典型。
另附 stage 锁告警的完整归因(不是内核缺陷,成因在插件进程侧运行时)。
3. **未完成的项 → `future-plan.md` §六**
尤其 `entities-retirement-plan` 那条:方案是"三表全退",但 2026-10-31
实测生产库 `entities 1292 / relations 978 / sentences 66` 行**仍在用**。
这个事实不能随文档一起消失。
## 保留两份(它们不是计划,是依据)
- `legacy-table-retirement.md` —— 被 **4 处生产代码注释**引用(`graph.go` ×2、
`commit_blocks_test.go`、`probe_prod_test.go`),是退役判断的依据
- `interrupt-subsystem-split-plan.md` —— 探测事实被 `interrupt_invariant_test.go`
引用;拆分本身确实未实施
## 索引重写
`docs/zh/README.md` 现在是「四类文档」的规则说明:
| 类型 | 回答什么 | 命名 |
|---|---|---|
| 设计文档 | 为什么这样设计 | `*-design.md` |
| 操作手册 | 怎么做 | `*-runbook.md` |
| 教训 | 踩过什么坑 | `engineering-lessons.md` |
| 未做登记 | 还剩什么没做 | `future-plan.md` |
并明确写「**不放执行计划**」及例外条件。
## 顺带修掉的引用
删除后 4 处引用指向不存在的文件:
- `deploy-runbook.md` → 改指 `toolcall-contract-and-sequence-design.md`
- SDK 仓 `guide/stream-tool-call-index.md` / `guide/parallel-tool-declaration.md`
→ 同样改指(这两个在 SDK 仓,随后单独提交)
## 验证
- docs/zh 27 篇,索引里列的全部存在、无遗漏
- 全仓无指向已删文档的引用(代码 + 文档)
- 代码块全部配平
- go build 通过
|
2026-10-31 08:50:39 +08:00 |
|
|
|
4963d70215
|
docs: 逐篇核实全部文档的完成状态 —— 修正 6 处与代码不符的自述
## 上一轮的遗漏
上次只补了"缺状态行",但**32 篇正文里的状态声明基本没动**。而那些声明
大多是 1~2 周前写的,代码早已推进。本轮逐篇对代码核实。
## 逐篇核实结果(方法:读当前代码,不看文档的历史描述)
### 1. memory-usability-plan:待执行 → 已实施
文档写「状态:**待执行**(2026-10-22)」。三条根因实测**全部已修**:
| 根因 | 实测 |
|---|---|
| R1 向量化输入是 jieba 词袋 | ✅ `coldcontext.go` 的 `denseFor` 明写「用事件正文(原文),不用 Summary(jieba 词袋)」 |
| R2 媒体块无语义 | ✅ 块 union 化,媒体即块自带描述 |
| R3 提示词无引导 | ✅ `tooldefs.go` 明写 `coldcontext_query` 把历史**插回时序**,并下沉到 kernel-memory skill |
### 2. entities-retirement-plan:执行中 → 方案已定、执行未完成
写「三表全退,执行中」,容易被读成"快完成了"。实测生产库:
`entities 1292 / relations 978 / sentences 66` 行**仍在用**。
迁移工具就绪,但生产未切换 —— 这个区别对判断风险很关键。
### 3. scene-memory-fix-plan:部分落地 → 7 步里 5 步完成
逐条核实:步骤 1/2/3/5/6 已完成(含 SDK 的 `ScenePolicy`、
`scene_emerge.go` 已区分 `peer:group_*` 与 `peer:user_*`);
步骤 4(清现网垃圾场景)与 7(收口验证)属现场操作,未复核。
### 4. prod-migration-checklist:⚠️ 最危险的一处
写「**状态**:HologramAgent 已停机(inactive + disabled)」——
那是一次性迁移(2026-10-04)当场的真实状态,**不是现在的**。
照着它读会以为服务停着。改为明确归档说明 + 指向现行 runbook。
### 5. abstention-criterion:「拒答尚未接入生产路径」已不成立
实测拒答已下沉到图库层(`RecallBlocksGuarded`),生产路径
`toolcall.go:1312` 与端到端探针**走同一条路**。保留了下沉的理由
(第一版放上层,探针绕过它 ⇒ 判据测不到被测路径)。
剩余两项(泛指词检测、n=10 样本偏小)仍开放。
### 6. toolcall-parallel-execution-plan:阶段 0.5 起为待办 → 全部已完成
实测阶段 0/0.2/0.5/1/2/2.5 与 3/4 全部完成,遗留项也已解决。
## 顺带修正两处写法
- `memory-restructure-plan.md`:顶部有「执行结果摘要」但正文各步骤仍写
`✗ 未开始`(原始计划快照,有意保留)。加一句读法说明,
避免读者拿正文的快照当现状。
- `future-plan.md` §六:新增 5 条实测确认的待办并去掉 2 条重复项
(遗留表退役、中断子系统原先各出现两次)。
## 核对
- docs/zh 32 篇全部有状态行,且与代码实测一致
- 代码块全部配平;文档间相对链接全部有效
- go build 通过(纯文档改动)
|
2026-10-31 08:20:47 +08:00 |
|
|
|
cf14b840ef
|
docs: 补齐文档状态分类 + 校正两轮代码改动后的失真
## 背景
上一次文档校订(b9b5ac0)之后又有两次代码提交(5194a96 / 8293282),
把 future-plan §1.1 整节变成了错的 —— 它还写着「档位没有生效点」
和「在 prepareInputTask 里加一次 activateSecurityProfile 调用」,
而后者正是被后来的收敛**否定掉的方案**。
## 新增 docs/zh/README.md(文档索引)
32 篇文档此前混着三种时态(当时实测 / 历史推演 / 设计规划),外观相同。
索引给出**状态分类与可信度规则**:
| 状态 | 能不能当事实用 |
|---|---|
| 已实现并部署 | ✅ 直接信 |
| 部分实现 | ⚠️ 只信标了"生效"的部分 |
| 设计 / 未实现 | ❌ 只能当设计意图 |
| 归档说明 | ❌ 不要改、也不要信 |
并写死**判据名可信度规则**:文档里的 `TestXxx` 不保证还存在。
给了核对命令,以及当前基线(27 条缺失的分布 —— 18 条属"设计未实现"类
文档的规划名、7 条是 §2.7.3 有意保留的历史对照表左栏、2 条属视频能力
未纳入契约)。判定标准:**在"已实现"类文档正文里找不到才是缺陷**。
## 重写 future-plan §1.1(已结案)
原节写的是错的方案。改成记录**真正的缺陷分三层**:
```
第 3 层(表面):activateSecurityProfile 零调用方
第 2 层:agent/core 在替 security 做策略决策
(自己抠 security_profile / 自己选判定链 /
自己写 Kind:"resident" 声明主体类别 —— 提权口子)
第 1 层(根因):security 缺少「输入接入点」
```
并补上「每 agent 一个实例」的最终形态(共享注册表 + per-agent 执行视图,
sessionProfile / longtimeProfile 两层,优先级 session > longtime 本身是安全语义)。
§〇「机制 ≠ 接线」表格里档位那一条从 🔴 改为 ✅。
## 各文档状态行
此前 14 篇缺状态行(runbook / 实测报告 / 计划类)。逐篇补上**符合实际内容**
的声明,而不是统一套一句。特别地:
- `commondev-design.md` 标题本就写「设计 · 未实现」→ 归到"设计"类
- `live-run-evidence-2026-10-04.md` → 明确标注"历史证据快照,不要用它的
结论覆盖现在的实现判断"
- `legacy-issues.md` → 写明它与 future-plan 的分工(前者收已知问题,
后者收能力缺失与零调用方)
## 正文里两处判据名漂移
- `TestBootCtxCloseAllIsReverseOrder` → 实际是
`TestKernelStateCloseAllIsReverseOrder`(`BootCtx` 改名时判据跟着改的)
- `TestNilTypedBusSafety` → 标注为"规划名,仓库中不存在"
## 新增 §2.7.5 模块产物的实例化粒度
依赖图保证"设施已就绪",但没说该有几个实例 —— 这是独立决策。
记下 security 的两个答案:全局基线共享(否则插件声明对部分 agent 不生效)、
轮次状态 per-agent。并给出给未来新模块的两条判断标准。
## 安全文档
状态头补「架构形态」段(防火墙定位、per-agent 视图、两层档位);
接线状态表里 `ActivateProfile` 从 🔴 改为 ✅(2026-10-31 接通)。
## README
文档列表加索引与 future-plan 入口 —— 此前从 README 找不到 docs/zh 的任何指引。
## 核对
- docs/zh 32 篇全部有状态行
- 索引里列的文档全部存在,无遗漏
- 分类与各文档自述一致(逐篇抽查)
- go build 仍通过(纯文档改动)
|
2026-10-31 07:50:58 +08:00 |
|
|
|
82932821c0
|
feat(security): 每 agent 一个策略实例 —— sessionProfile / longtimeProfile 两层
## 用户裁定
> 每个 agent 一个,分为 sessionProfile 和 longtimeProfile
## 收敛前的缺陷
档位挂在 `Runtime.active`(**进程级单例**)上,多 agent 并发时串档:
主 agent 轮次 A:注入 qq.guest → active = "qq.guest"
驻留子 轮次 B:注入 qq.owner → active = "qq.owner" ← 覆盖 A
主 agent 轮次 A 继续执行工具 → 判定按 owner 走 ← 串档
这不是理论风险:驻留子 / rtchild 与主 agent **同进程并发**(plan.md 十一)。
## 切分原则:共享注册表 + per-agent 执行视图
**不能整份复制 Runtime** —— `profiles` / `principals` / `scopes` 是插件
声明出来的注册表,每个 agent 各持一份就会变成「插件往 A 声明了档位,
B 看不见」,那是最难查的一类缺陷。
共享(进程级 1 份) per-agent(N 份)
──────────────────────── ───────────────────
gate / policy / content sessionProfile ← 轮次级
profiles / principals longtimeProfile ← agent 生命期
scopes / eventPolicy
## 两层档位语义(用户裁定)
sessionProfile —— 属于**这一轮**。输入携带它,End() 即清。
严格 per-round:同一用户下一条消息要重新声明。
longtimeProfile —— 属于**这个 agent 的一段生命**。设置后跨轮保留,
直到显式改掉或 agent 销毁。
**优先级 session > longtime**,这个顺序本身是安全语义:
session 是"这一轮的事实",必须能盖住 long-time 默认值 ——
否则访客发一条消息也无法在长期为 owner 的 agent 上收窄权限。
反向(longtime 盖 session)= 提权:长期 owner 的 agent 上,
访客声明的 guest 被吞掉 ⇒ 访客拿到 owner 权限。
## 改动
① 新增 `internal/security/agentscope.go`:
rt.ForAgent(agentID, isChild) *AgentScope // 幂等
sc.BeginRound(RoundInput) // security 自己解析档位
sc.SetLongtimeProfile(name) / LongtimeProfile()
sc.EffectiveProfile() // session > longtime
sc.CheckTool(tool) // 全局基线 + 档位 + 主体策略
rt.ReleaseAgent(agentID) // 含 longtime 档一起清
② `Runtime` 新增 `scopesByAgent` 表;判定拆出 `checkToolWithProfile(p, profile, tool)`
与 `checkToolForWithProfile(...)` —— **档位由调用方显式传入**,
不再一律读全局 `r.active`(否则 per-agent 视图白做)。
③ `agent/core`:`securityScope()` 用 `sync.Once` 缓存本 agent 的视图;
`Stop()` 里 `ReleaseSecurityScope()`。
④ `Round.End()`:per-agent 路径只清 **session**,**保留** longtime;
旧进程级路径仍恢复进入前的值。
## 判据(3 个变异全部实测咬住)
| 判据 | 变异 |
|---|---|
| `TestAgentScopesDoNotShareSessionProfile` | ForAgent 退化为共享视图 ⇒ 红 |
| `TestLongtimeProfileSurvivesRounds` | 优先级写反 / longtime 不跨轮 ⇒ 红 |
| `TestReleaseAgentDropsLongtimeProfile` | ReleaseAgent 不删视图 ⇒ 红 |
| `TestLongtimeProfileRejectsUndeclared` | 未声明档位被接受 ⇒ 红 |
第一条同时钉:① 各自档位是自己的 ② **行为上**主 agent 仍被 guest 收窄、
子 agent 不受影响(只断言字符串相等会漏掉判定链没接上的情形)。
## 实测
- 真实并发探针:3 个 agent(guest / owner / guest)各 200 轮并发,
`-race` 干净,**零串档**
- build / vet / go test ./...(62 ok / 0 fail)/ -race 全绿
## 未变的约束
档位与主体策略**只能收窄**:`CheckToolFor` 内部先跑全局策略,
所以父的黑名单对子依然有效,子无法提权。
|
2026-10-31 07:24:03 +08:00 |
|
|
|
5194a96560
|
refactor(security): 安全策略全面收敛到 security 模块 —— agent 只提供事实,不再做策略决策
## 架构定位(用户澄清)
security 是**外围内核到核心内核之间的防火墙**:自己提供接口、判定内聚在自己内部。
device 是设备抽象模块、持有 channel,两者不冲突。
现成的成功范例就在树里:`events.Bus.SetGuard` + `security.AsEventGuard` ——
总线只交事件,guard 自己判,总线一行策略都没有。本次把同样的形状套到输入/主体上。
## 收敛前的问题
`agent/core/security_profile.go` 214 行里绝大部分是**策略逻辑**:
name, _ := payload["security_profile"].(string) // ① 解析 security 的协议
a.security.ActivateProfile(name, 0)
if a.parentID != "" { // ② 选哪条判定链
return a.security.CheckToolFor(Principal{Kind: "resident"}, tool)
}
a.security.DeclarePrincipalPolicy("id", id, PrincipalPolicy{...}) // ③ 拼策略结构
三个后果:
① **协议解析外泄**:`security_profile` 是 security 的概念,解析却在外人手里
② **策略决策外泄**:判断"父走 CheckTool / 子走 CheckToolFor"是策略语义
③ **提权口子**:`Kind: "resident"` 是调用方**声明自己的类别** —— 说自己是
内部主体就享受内部豁免(不过 HardPrivateTools)。类别必须从事实推出
且三条输入路径(主 agent / 驻留子 / rtchild)各自得接一遍,接漏一条就静默失效。
## 收敛后的形态
security 新增 `round.go`(自己提供接口):
RoundInput{AgentID, IsChild, Payload, Source} ← 只有事实,**无 Kind**
rt.BeginRound(in) → security 解析档位 + 判定主体类别
round.CheckTool(t) → 本轮判定(调用方不需要知道任何组合规则)
round.End() → 归位(防跨轮泄漏)
rt.DeclareChildPolicy / RevokeChildPolicy ← 子策略生命周期
agent 侧只剩:`beginSecurityRound(payload, source)` + `defer End()`。
`security_profile.go` 从 214 行策略逻辑退化为转发层。
顺带全局收敛 `Principal.Kind`:裸 string → 固定枚举
(KindInternal / KindSubAgent / KindUser / KindGroup),并新增
`ParsePrincipalKind` —— 拼错返回 false 而不是静默回退到某类别。
## 轮次边界接到**全部三条**输入路径
task.go runInputTask ← 主 agent + 驻留子(各有自己的 Agent 实例)
rtchild.go rtRunOnce ← rtchild 不走调度器,是独立的一条
★ 收敛把**判定逻辑**收到了一处,但"一轮何时开始/结束"仍得各路径自己声明 ——
只有该路径知道边界。所以判据必须钉"每条路径都声明了"。
## 判据(5 个变异全部实测咬住)
| 判据 | 变异 |
|---|---|
| `TestSecurityProfileFromInputChangesToolDecision` | BeginRound 不解析 payload ⇒ 红 |
| `TestProfileRestoredAfterRoundEnd` | End 不归位 ⇒ 红 |
| `TestUnknownSecurityProfileFallsBackWithoutPanic` | 拼错沿用旧档 ⇒ 红 |
| `TestAgentCoreHoldsNoPolicySemantics` | 把 `Kind:` / `CheckToolFor(` 写回 agent ⇒ 红 |
| `TestPrincipalKindIsDerivedBySecurity` | 分类规则错位 / 枚举接受拼错值 ⇒ 红 |
| `TestEveryInputPathDeclaresRound` | rtchild 路径漏 beginSecurityRound ⇒ 红 |
`TestAgentCoreHoldsNoPolicySemantics` 逐行扫描(跳过注释),禁四类形态:
`payload["security_profile"]` / `Kind:` / `.CheckToolFor(` / `PrincipalPolicy{`。
## 实测(真实整机装配)
① security 自己解析出的档位 = "qq.guest"
② security 自己判定的主体 = {ID:main Kind:user}
③ 本轮判定 cmd_run = Allowed:false
reason="tool \"cmd_run\" is disabled by profile \"qq.guest\""
## 已知限制(已写进 round.go 与 future-plan)
`Round` 是 per-round 句柄,但 `Runtime.active` 仍是**进程级单例**。
多 agent 轮次**交错**时无法表达两个不同档位(串行场景完全正确)。
彻底修法是把档位挂到 TaskFrame 上按任务隔离,需先改 CheckTool 签名。
build / vet / go test ./...(62 ok / 0 fail)/ -race(security + agent/core +
kernelmod/all)全绿;make build 通过。
|
2026-10-31 07:14:09 +08:00 |
|
|
|
b9b5ac0282
|
docs: 全量校订文档到当前代码状态 + 建立 future-plan 作为唯一未做登记处
## 为什么
文档里混着三种时态:当时实测、历史推演、描述现状但已失真。三者外观相同,
读者无从分辨。本次逐条用实测核对,把失真的改成事实,把「已做」从「未做」
里摘出来,把「真没做」集中登记到一处。
## 新文件:docs/zh/future-plan.md
此前「未做」散落在各设计文档的「待办/剩余/遗留」小节,时间一长就与代码
脱节(已完成却仍写未做,未做却被当成已完成)。新建本文作为**唯一登记处**,
收录规则明确:只收「代码里确实没有」与「有机制但零生产调用方」两类。
它同时把本仓一个反复出现的缺陷形态显式化 —— **机制 ≠ 接线**。已攒够 5 例:
| 能力 | 曾经/现在的状态 |
|---|---|
| `Policy.CheckTool` | 曾 `Agent.security` 恒 nil → 2026-10-30 修 |
| `CheckToolFor` | 曾 0 生产调用方 → 2026-10-30 修 |
| `ActivateProfile` | **现在仍 0 调用方** |
| `ScanContent` | **现在仍 0 调用方** |
| `CheckPath` / `CheckNetwork` | **现在仍 0 调用方** |
## 已失真的表述(逐条实测后更正)
**kernel-modularization-design.md**
- §2.7.4 原标题是「未做(Step 4b 的剩余)」,写着「main.go 仍走原来的串行代码,
下一步是把 Run(plan) 真接进 main」。**实际 `main.go:229` 早已调用
`RunKernelModules` → `kernelboot.Run`**,插件调度也已归位 plugin-host。
改为「已完成」并给出两处代码位置 + 生产日志证据。
- §2.7 分层数字「6 层 / 17 模块,最宽层 9」→ 实测 **20 模块 / 6 层 / 最宽 10**。
同时给出取数命令,并要求「数字一律现场取,不凭文档记忆」。
- §2.3.5 那段标着「当前已产出的分层结果(实测)」的 4 层 14 模块表,是
2026-10-16 的推演。改为明确标注「已过时,仅作设计推演保留」。
- §2.7.3 判据表 7 条**全部不存在**(随 3864f36 删死包迁包时丢失)。改为
左右栏对照:左边当时名字、右边现仓库真实判据,找不到对应物的 4 条明确标 ❌。
- 文件头补状态声明、与 future-plan 的分工、数字取数方式。
**kernel-secret-firewall-design.md**
- 状态从「已实现」改为「部分实现」。原来那句会让读者以为整套安全能力都在跑。
- 新增接线状态表:逐个写**实测的生产调用点**(不写 ✅/❌ 定性)。
事实是:凭据三件套与工具闸生效;**敏感词/注入、路径沙箱、网络闸三条
机制完整但零生产调用方**;档位声明侧通、激活侧零调用方。
- §5 T1–T10:T2/T3/T4 早已落地却仍挂在「待办」表里。改为现状列,
并把真未做的 T5–T10 迁入 future-plan §1.5。
**对外文档的工具名**(外部读者正在看错的名字)
- `ARCHITECTURE.md` 6 处、`OVERVIEW.md` 1 处、`README.md` / `README_EN.md` 各 3 处、
`resident-subagent-design.md` 2 处:`memory_*` / `doc_query` → `stablemem_*` / `coldcontext_*`
- README 的 v1.1.1 发布说明保留原名(那是历史事实),加一句现名注记
**input-scheduler-design.md / resident-subagent-design.md**
- 补状态头。input-scheduler 记入 rtchild 不走四级阶梯(单槽覆盖)这一下游差异;
resident-subagent 记入「继承安全运行时实例但**不继承权限**」这一下沉事实。
## 未动的(有意为之)
历史归档文档(`legacy-*`、`*-plan.md`、`live-run-evidence-*`、
`production-recall-validation.md`、`context-overflow-l4-design.md`、
`entities-retirement-plan.md`、`memory-*-plan.md`)**一律不改** ——
它们是历史记录,改了就篡改证据。其中的待办以索引形式登记在
future-plan §六,原文档保留。
## 核对结论
- 文档现在只描述两种内容:**已落地的实现**(附实测证据/取数命令)与
**尚未实现的事项**(统一在 future-plan)
- 数字类断言全部给出取数命令
- 跨文档的「未做」引用统一指向 future-plan,避免再次分叉
|
2026-10-31 06:58:17 +08:00 |
|
|
|
e78a38b3d8
|
fix(security): 父为子声明工具策略 —— 修正上一版「子继承父权限」的错误模型
## 上一版错在哪
上一版(1d007e1)给子 agent 加了 `Security: a.security`,判据也钉的是
「子持有**同一个** Runtime 实例」。
那只保证了**机制**到位,却把权限模型写错成了「子 = 父的完整权限」。
用户裁定:**应当是父 agent 为子 agent 声明策略**。
区别是实质的:
错的模型:父能调 cmd_* ⇒ 子也能调 cmd_*(子只受全局策略约束)
对的模型:父在创建子时决定这个子能调什么,内核持有并执行,子不能自己扩大
驻留子长期无人盯、rtchild 通话中一边听一边执行工具 —— 它们恰恰最需要
**比父更窄**的权限,而不是与父等权。
## 这不是新机制,是没接线的旧机制
`security.Runtime` 里早就有:
CheckToolFor(p Principal, tool) // 按主体判定
PrincipalPolicy{AllowTools, DenyTools, // 主体策略
HardPrivateTools}
DeclarePrincipalPolicy(scope, key, policy) // 声明入口
而 `codegraph_callers("CheckToolFor")` = **0 个生产调用方**
(与工具策略闸、ScanContent 同型:机制完整、接线缺失)。
公开 SDK 也没暴露 `DeclarePrincipalPolicy`(插件暂时无法声明主体策略)。
## 改动
① `security`:新增 `RevokePrincipalPolicy(scope,key)` / `HasPrincipalPolicy(id)`。
**撤销是必须的**:主体 ID 是调用方给的名字(`create id="bot1"`),
子回收后同名重建是常规操作,旧策略留着 ⇒ 新一代用上一代父留的约束运行。
这与今天修的 rtchild 留档串线(`rtClosedTranscripts` 以模型给的名字为键、
全仓 0 处删除)是**同一种形态**。撤销只支持 scope="id" —— 允许撤销 tag
等于任何执行体销毁时都能抹掉全站策略。
② `agent/core`:`securityDecisionFor` 分两条路径
父(parentID == "") → CheckTool(tool) // 内核自身
子(parentID != "") → CheckToolFor(Principal{self, "resident"},
tool) // 叠加父的声明
CheckToolFor 内部**先**调 CheckTool,所以主体策略无法绕过全局黑名单。
主体 Kind 用 "resident"(IsInternal 为真):HardPrivateTools 不拦内部委派
(那条防的是用户主体偷看私人数据),但父声明的 Deny/Allow 仍然生效。
③ 创建点声明、销毁点撤销(生命周期对称)
SpawnResident → declareChildPolicy(opts.AllowedTools, opts.DenyTools)
SpawnRTChild → 同上
teardownResidentWith / StopRTChild / StopRTChildren → revokeChildPolicy
声明**必须在子开始执行之前**(rtLoop / child.Start 之前),否则存在一个
「子已跑工具、策略还没进内核」的窗口。
声明失败 ⇒ 回滚整个创建:一个「本该受限却无限」的执行体比创建失败危险得多。
④ 工具面:`resident_agents create` 与 `rt_children create` 新增
`allowed_tools` / `deny_tools`。描述里写明「**你为它声明**」而非「授权」,
因为模型很容易把白名单误读成额外授予。
## 判据(4 个变异全部实测咬住)
| 判据 | 变异 |
|---|---|
| TestParentDeclaresChildPolicy | 子退化为 CheckTool(不分主体)⇒ 红 |
| TestChildTeardownRevokesPolicy | teardown 不撤销 ⇒ 红;StopRTChildren 漏撤销 ⇒ 红 |
| TestPolicyDeclaredBeforeChildStarts | 声明挪到 rtLoop 之后 ⇒ 红 |
| TestPrincipalPolicyCanBeRevoked / TestRevokeRejectsTagScope | security 侧撤销语义 |
`TestParentDeclaresChildPolicy` 同时钉三件事:① 父声明的 deny 拦住子
② 同一条策略**不影响父**(主体隔离,不是全局策略)③ 撤销后子恢复。
`TestChildTeardownRevokesPolicy` 用源码断言单列,因为本仓反复吃亏在
「单数路径改了、复数路径漏了」—— `StopRTChild` 注销入站登记而
`StopRTChildren` 漏了,就是这么来的。真销毁需要完整记忆栈,而
「前置不满足就 skip」的判据等于没有。
## 实测(真实整机装配 + 真执行)
① 父 ExecuteTool(cmd_run) → 走到分派(未被拦)
② 父声明 deny=[cmd_run] 后,子的判定 → Allowed=false
reason="tool \"cmd_run\" is not permitted for this principal (deny \"cmd_run\")"
③ 父自己的判定 → 仍 Allowed(主体隔离)
④ 撤销后子的判定 → 恢复 Allowed
build / vet / go test ./...(62 ok / 0 fail)/ -race(kernelmod/all +
agent/core + security)全绿;gofmt 漂移清零。
## 仍未接线
- `ScanContent`(敏感词/注入预警)仍 0 生产调用方
- 公开 SDK 未暴露 `DeclarePrincipalPolicy`(插件无法声明主体策略)
- `OversizeToolReports` / `oversizeHintFor` 仍 0 消费者
|
2026-10-31 06:35:32 +08:00 |
|
|
|
1d007e1d49
|
fix(security): 工具策略闸真的接上了 —— 根因是依赖图少一条边,不是少一行赋值
## 缺陷
生产日志实证:`[security] module enabled (31 variables)`、`[device] ready:
34 tools, security=true`,691 个日志文件里 `denied by policy` 命中 **0 次**。
`executeToolCallInner` 里的策略闸读 `a.security`,而它**永远是 nil**:
codegraph_callers("SetSecurity") → 4 个调用方,全是 security_toolgate_test.go
全仓唯一给 a.security 赋值的行:security_profile.go(就在 SetSecurity 体内)
buildAgent 完全不读 k.Security;AgentConfig 里也没有 Security 字段
## 为什么判据全绿而生产是坏的
`security_toolgate_test.go` 的 4 条判据**自己调了 a.SetSecurity(...)** ——
它们把缺失的那根线补上了,所以永远绿。测的是「接线之后行为对」,
不是「生产有没有接线」。这是「机制 ≠ 接线」最纯粹的样本。
## 根因:模块边界没连上,不是少一行
`kernelmod/agent` 的 Requires() **没有声明 FacSecurity**。依赖图因此没有
理由把 security 的产物交给 agent —— 只有声明了的 device 拿到了。
即使不声明,security 也在第 1 层、agent 在第 4 层,`ks.Security` 恰好已被
填好,运行期照样能读到。**这正是它藏了整轮发布的原因**:那个"恰好"是偶然
(security 只依赖 config),不是设计。一旦 security 将来加了自己的依赖,
图不会拦,只会静默给出 nil。
所以只补 `agent.SetSecurity(k.Security)` 是错的:那会让代码读一个图不保证的
字段。必须同时改三处。
## 改动
① kernelmod/agent: Requires 加 FacSecurity(让图知道这条边)
② AgentConfig 加 Security 字段;New() 写 a.security;buildAgent 传 k.Security
③ Init 里 ks.Security == nil 时**响亮失败**(与 device 模块对 ks.Security
的检查同型)—— 不能把 nil 一路传下去变成静默放行
④ 子 agent 继承:resident / rtchild 各自 New(AgentConfig{...}),原先从构造起
a.security 就是 nil。它们是**最需要闸门**的执行体(驻留子长期无人盯、
rtchild 通话中一边听一边执行工具),却恰恰完全没有闸门。
## 判据(全部通过变异验证,删掉修复必红)
新增 internal/kernelmod/all/security_wiring_test.go(钉**装配**,不是钉注入):
| 判据 | 变异 |
|---|---|
| TestAgentDeclaresSecurityDependency | 从 Requires 删 FacSecurity ⇒ 红 |
| TestAgentAssemblyWiresSecurity | buildAgent 传 nil ⇒ 红;New() 不写 a.security ⇒ 红 |
| TestSubAgentsInheritSecurity | 删 resident/rtchild 的 Security ⇒ 红 |
| TestAgentAssemblyRefusesNilSecurity | 删 Init 的 nil 检查 ⇒ 红 |
`TestAgentAssemblyWiresSecurity` 走**真实登记表装配**(真 SQLite / 真配置库 /
真 20 模块 6 层),并对一个**未注册**的工具名配黑名单后调真实 `ExecuteTool`:
闸门生效 ⇒ "not permitted";闸门失效 ⇒ "does not exist"。两者文本不同,
无从混淆,因此不需要注册探针工具。
## 实测
生产形态探针(配置写入 `core.security.tool_deny=cmd_run` 后启动真实二进制):
[security] global policy applied: deny=[cmd_run] allow=[] sandbox=false
Agent.ExecuteTool("cmd_run") → denied (Tool cmd_run is not permitted:
tool "cmd_run" is blocked by security policy (denylist entry "cmd_run"))
分层未变(3+10+3+1+2+1 = 20 模块 6 层,digest 14b4f6f7),
build / vet / go test ./... / -race(kernelmod/all + agent/core + security) 全绿。
## 顺带
gofmt 漂移清零(原 1 个 rtchild_route_test.go)。
|
2026-10-30 23:54:08 +08:00 |
|
|
|
fbccfe17ed
|
fix(rtchild): 同名复用串线 + 批量停止漏注销
三处缺陷,全部在「回收/复用」这条线上 —— 单实例 e2e 天然测不到:
1. RTTranscript 读序(主线)
原实现先查留档、命中即返回。留档以 id 为键而 id 由模型给,于是
回收后同名重建,rt_transcript 返回的永远是上一通电话的原文,
且完全不报错。改为「在跑实例优先,无活实例才落回留档」。
2. StopRTChildren 漏注销入站登记
单数路径会 Unregister(child/rt-<id>),复数路径没有。登记表共享、
按 name 全局唯一,残留随 create/destroy 单调累积,而外部没有任何
工具能单独注销一条 inputch。
3. 撤回一个错误的修法
一度在 SpawnRTChild 里 delete 留档。变异测试证明删掉它判据仍绿
(治不了串线),且有害:留档的意义正是「回收后父按需取原文」,
提前删掉会造成「推了信号却拉不到正文」。现仅在 CloseRTChild 写
同名键时自然覆写。
判据(均通过变异验证,去掉修复必红):
- internal/agent/core/rtchild_reuse_test.go(进程内,5 条)
- internal/plugins/rtchild_reuse_e2e_test.go(跨进程真插件,3 条)
★ 写判据时踩到两个坑,一并记在注释里:
- 前两版判据假绿(先塞留档被 Spawn 清理掉 / 新实例不 push 导致
「读到旧内容」与「读到空」都是 len==0),靠变异才发现没测到真东西
- 等 Rounds 不等于历史已入账(rtRunOnce 先 round++ 后 rtAppendHistory),
与 dff745c 同一个坑;已改为等 history 条数
|
2026-10-30 21:54:40 +08:00 |
|
|
|
bcf45498a0
|
feat(skill): 补 kernel-rtchild + 两条判据锁住 rt_children 静默失败
## kernel-rtchild skill
`resident_agents` 有 `resident-subagent-hygiene` skill,而
`rt_children`/`rt_transcript` 这一对整个**没有 skill** —— 模型只能靠
工具描述猜语义。
最容易踩的坑:**工具执行期不可打断**。模型一旦进入工具执行期就听不见
新输入,所以调工具前必须先用 outputch 说一句"稍等",否则用户会觉得
"它没在听我说话",而实际上它只是卡在一个 3 秒的命令里。
skill 覆盖:三种子 agent 的分界、通道必须成对且两侧声明 rt、打断语义、
只出文本(TTS 是插件的活)、窗口裁剪但原文保留、回收后用 rt_transcript
分段取、闲置零 token 但必须 stop、四条回收判据、五种常见错误对照表。
## 两条判据(都来自一次真实事故)
生产 e2e 时我用 `input="..."`(工具声明的是 `inputch`)创建 rtchild:
`strArg` 只查精确键名 ⇒ 读到空串 ⇒ **本应报"必须成对给出",但它成功了**,
还把 output 通道划给了新子。之后灌语音,该子轮次永远是 0。
症状是"通道注册了、插件活着、推送 ok、但子没反应",
与真因(参数名拼错)隔了三层。
- `WrongParamNameIsRejectedNotSilent`:写错必须失败、必须指名正确参数名、
必须无副作用(不建子、不动通道归属)
- `PairAssignmentIsAtomic`:成对分配第二个失败时第一个必须回滚
(原来那句 `rollback()` 没有判据覆盖,是死代码)
变异验证:让代码对 `input`/`output` 静默兜底 ⇒ 第一条变红
(`参数名写错却创建成功了:[typo-child] —— 这会得到一个收不到输入的哑子`)。
go build/vet/gofmt 干净;go test ./... -count=1 -race = 62 ok / 0 FAIL / 0 RACE
|
2026-10-30 18:09:50 +08:00 |
|
|
|
dff745cde2
|
test(rtchild): rt_children stop 判据改为等历史入账,不等 Rounds
判据偶发红(实测约 1/12):`stop 没有告诉模型用 rt_transcript 取回原文`。
根因是**判据自己等早了**,不是产品缺陷:
- `Rounds` 在 ChatStream 被调用时就自增(请求刚构建完)
- assistant 那条消息是**流结束后**才追加进 history 的
于是 stop 可能读到 n==0,走"本次通话没有上下文需要交接"分支 ——
而那个分支**本来就该**不提 rt_transcript(真没内容可取)。
改为等 RTTranscript 非空。
★ 这是同一类坑的第二次复发(第一次在 rtchild_r7b_test):
判据等"请求已发出"而不是"结果已落库"。
"""
|
2026-10-30 17:21:51 +08:00 |
|
|
|
eb1deab5dd
|
feat(agent): rtchild 实时子 agent + 四类 agent 独立模型源 + 残余任务四态
## rtchild(R1–R13)
第三种、**独立**的子 agent 类型,面向语音/视频通话:内核只要文本流,
不定音频(TTS/播放由插件负责)。
- R1 CapRT 能力位 + AgentConfig.IsRealtime;AssignRealtime 两侧校验
- R2 rtSlot 单槽覆盖(latest-wins,从不排队)
- R3 独立 ctx + rtLoop + SpawnRTChild/StopRTChild
- R4 ChatStream 逐块 → outputch(rt).Execute(text)
- R5 打断 = 原地截断,不重试
- R6 工具执行期不可打断(suppressedCancels 让守卫可观测)
- R7 普通上下文 + 窗口裁剪(旧内容不进模型,**原文保留**)
- R9 CloseRTChild → L4 信号 + rt_transcript 按需分页取
- R10 notify_parent(L4,注册一次,停即注销)
- R11 闲置零 token
- R13 rt_children 管理工具(父专属)
刻意**不做** rtPage/anchor/degraded_resets(过设计,已裁定);
不用四级中断阶梯(preemptCooldown+同级不抢占会让 ~75% 的 rt 打断
静默退化为排队)。
## 四类 agent 各自可配模型源
core.agent.llm.{main,resident,rt,child}_provider,语义是**优先不是独占**:
该源排到候选链最前,其余源仍作回退。硬锁定会让"首选源抖一下"变成
"这个 agent 彻底不工作",而多源回退链正是本仓为此建立的机制。
名字写错 ⇒ 回退默认源 + 记日志(不静默)。子 agent 未配置时继承父的。
★ 顺带修掉一个真缺陷:spawn.go 原来直接 a.provider.Chat(),
**普通子 agent 完全绕过 provider 回退链** —— 它是唯一绕过的地方:
默认源抖一次整条子任务就"执行失败",而主 agent 同刻能自动换源。
## 驻留子残余任务四态 + 通道三态
残余任务:keep / takeover(接手,keep 的明确别名)/ stash(暂存到各自
原通道)/ drop。stash 必须**逐条按各自 OutputChannel 归位** —— 一个子
可能同时收 qq 和 webui,混存会让"接回 qq 的人"拿到本该给 webui 的活。
通道:release(零值,向后兼容)/ suspend(挂起,暂存保留)/ takeover
(接回父并立即释放暂存)。原来是无条件 release,于是只有两种坏结局:
通道一直没人分配(消息无人处理且无提示),或刚好被别的 agent 抢到。
一次定完不留窗口:FinishResidentWith → DestroyResidentWith →
teardownResidentWith。任一环退回不带 disp 的旧函数,disp 就被丢掉、
通道被无条件释放 —— 而 stash 的暂存正挂在那条通道上(实际踩过)。
## 修复"手写字段白名单"丢值(7 处)
同一类缺陷:不传整个结构体,而是手写一份"允许通过的字段清单"。
危险在于**写漏了不报错** —— 字段静默归零,故障出现在很远的地方。
- corehandler.go outputRegister: ChannelDef{NoMemory,Cleaner} → 整体传
- pubSdkInjectOpts: 逐字段摊平丢 ScenePolicy/SecurityProfile → 收整个 injectParams
- injectParams/injectMediaParams: 结构本身缺字段 → 补齐 + asInjectParams()
- 配套反射判据 TestSegment_RoundTripEveryField(取代手写断言,
原判据漏了 Phase/LLMText/FinalText/ToolCalls/ExtraKeyMediaBlocks/ExtraBag)
## 压缩偏离设计
CompressResident 原为截断,与设计(LLM 摘要替换)不符。改走
PlanCompaction → summarizeForCompaction(用子自己的 provider)→
ApplyCompaction,失败回退 TrimKeepRecent。顺带修 ApplyCompaction 的
锁重入 panic 与双重解锁。
并修一处真实生产数据竞争:ctxFullSignaled 由父 goroutine 写、
子调度 goroutine 读,加 ctxFullMu。
## 判据
新增约 70 条。全部验证过能变红(变异测试),包括:
- TestSegment_RoundTripEveryField 的防腐蚀:fullStageContext 反射断言
每个字段非零,否则新字段永远不会被覆盖("假绿")
- TestPreferredProvider_PutsSourceFirstAndKeepsFallbacks 成对断言
"在首位且回退仍在"—— 只断言前者的话,硬锁定也会绿
## 判据自身的坑(两次偶发红,都不是产品缺陷)
1. SpawnResident 末尾 Start(),子会在两次 enqueue 之间取走一条
→ "暂存 0 条"。修法:cancel + waitSchedulerStopped。
(offload_test.go 早有记录,我没先读就重踩了一遍)
2. TaskPrompt 异步投递,可能在清空队列之后才到 → "多 1 条"。
靠探针打印内容才认出是 TaskPrompt。修法:本文件用 spawnQuietResident。
go build/vet/gofmt 干净;go test ./... -count=1 -race = 62 包 ok / 0 FAIL / 0 RACE
|
2026-10-30 17:00:47 +08:00 |
|
|
|
b850feaef2
|
feat: 段页式记忆流转、coldcontext 重命名与安全治理
记忆层:
- MemoryBlock 承载 FrameTS/PageSeq/PageKind/BlockRole/CallID
- coldcontext 归档与回填按完整页传递,工具调用参数和配对键不丢失
- 召回先估算总 token,超预算时按全局页相似度标记 inactive
- inactive 仅是 context 渲染期状态,不落盘、不进入 coldcontext,Prune 后重置
- Peek/确认/Remove 两阶段召回,失败保留冷上下文副本
- stablemem 记录 last_recalled_at,删块连带清理边
命名:
- internal/memory/document → internal/memory/coldcontext
- Doc/DocEvent/DocHit → ColdDoc/ColdEvent/ColdHit
- ContextToColdContext/RecentColdContexts/FindColdContexts
- 保留既有 JSON 标签、doc_ 文件前缀与 doc_count 状态键,兼容存量数据
安全治理:
- 工具策略门禁、安全配置、事件总线发布者身份与入站脱敏
- 声明式 SecurityAPI 与 QQ owner/guest profile
- 日志保留策略与相关 e2e/回归判据
验证:go build ./...、go vet ./...、go test ./...(62 包);
生产副本 e2e:7013 块迁移后 ID/向量/指纹 0 变化、0 缺失。
|
2026-10-29 23:53:48 +08:00 |
|
|
|
a3e50b395f
|
merge: 差分 OTA + 安装形态分派 + 事件总线统一受安全监控 (#32)
feat(update): 差分 OTA + 安装形态分派 + 事件总线统一受安全监控
|
2026-10-29 10:01:45 +08:00 |
|
|
|
ba281a855a
|
feat(update): 差分 OTA + 安装形态分派 + 事件总线统一受安全监控
三条主线,由 e2e 实测驱动。
## 一、差分 OTA(internal/update/delta.go)
实测:改一行逻辑,88MB 的二进制逐字节只差 80 字节,而 OTA 一直下整包。
用 rsync 族的滚动哈希块匹配(4KB 粒度),不是 bsdiff —— 后者需 17×N
峰值内存(88MB → 1.5GB),而升级恰可能发生在内存紧张时。
真实二进制实测:88,036,728 字节 → 补丁 77,316 字节(0.088%)。
三道闸:应用前验旧文件 sha256(错基线会静默产出垃圾)、应用后验结果
sha256、COPY/LITERAL 越界防护。任何失败**退回整包**,不向上抛 ——
差分是优化,不是可用性前提。
接入:Source.Resolve 优先试 delta;CheckRelease 按 -from-<commit8>.delta
匹配(不猜基线);CLI 新增 -make-delta;打包脚本 build_delta(需
PREVIOUS_HOMED),delta 进 SHA256SUMS。
## 二、安装形态分派(internal/update/install.go + channel.go)
内核对「能不能自己升级」此前只有一个答案:直接替换二进制。而 deb/rpm
形态下那样做会让 dpkg/rpm 数据库与实际内容不一致(下次 apt upgrade
覆盖回旧版,或 dpkg 拒绝后续升级)。
新增探测:deb/rpm/tar/container/source 五类形态 + 变体(full/server/
client/toolchain)+ 目录写权限。判据全部来自可观测系统事实(dpkg 的
info 目录、容器标记文件),不做路径推断。
三端统一走 PlanUpgrade 分派;形态闸门放在**下载 880MB 之前**。
## 三、事件总线统一受安全监控(internal/events + security)
用户指正:「eventbus 上所有事件都要接受安全模块的监控」。
此前 EventPolicy 只被 internal/sdk/events.go 调用,而那是四条路径中的
一条。s.Publish / s.Subscribe / s.EventBus() 拿的是裸 *events.Bus,
任何插件都能绕过策略发任意事件(含伪造内核事件)。
现在闸门在 Bus.Publish/Subscribe 本体上(Guard 接口注入,避免 events
↔ security 成环),四条路径全部覆盖。sdk.EventBus() 改返回带身份的
BusHandle。内核身份免授权,但载荷照样过凭据闸门。
白名单按主体收窄:agent_output/stage/terminal_output 只允许承载者
(webui/cli/agentcli)发布 —— 它们是内核向外的输出面,谁都能发就等于
能伪造 agent 说的话。
## e2e 暴露并修掉的三个真实回归
[bus] subscribe denied: pluginmgr type=update_request
[bus] subscribe denied: skillmgr type=skill_detected
前两个让升级请求静默丢失、职责链交接断掉。内部协作事件必须可订阅
(安全边界在发布侧,订阅只是「听得到」)。
## 顺带的止血修复
- 回滚判据从 `--version` 改为 digest:旧内核(1.3.x 及以前)没有该
flag,而它恰是最常见的回滚目标 —— 原逻辑让「从旧版升级失败」永久
无法回滚。
- CopyFile 补 fsync(替换 homed 本体时掉电会留半截文件)。
- restore 完整还原才清 marker(原先先清再还原,还原失败就失去第二次
自愈机会)。
- 回滚时移除升级中新增的 depgraph(否则「旧 homed + 新 depgraph」
会因 digest 不匹配崩溃循环)。
- homed-upgrade 不再硬编码 systemctl restart(丢 guard 状态、unit 名
漂移);改用内核已有的 restartCh → exit 42 语义。
- homed-upgrade 不再用 CGO_ENABLED=0 编却依赖 go-sqlite3(stub 下
静默失效,该路径从未工作过);改用 internal/config。
- 服务单元补 User=hologramagent + 三条安装路径补建用户(此前发行物里
完全没有落实降权)。
- rpm 的 fpm 调用补 --after-install(此前 rpm 装完根本不建用户)。
## 判据
新增 30 条:差分 8(往返/压缩比/错基线/截断/篡改/边界)、通道 8、
执行器 6、事件闸门 8、按主体授权 8。全仓 go test ./... 0 失败。
|
2026-10-29 09:54:55 +08:00 |
|
|
|
7f68d2b1bf
|
test(proc): 流式发布耗时判据改为 race 感知阈值
CI 红(run 40473247694,Race detector job):
--- FAIL: TestStreaming_PublishLatencyFlatAcrossSubscribers
1 个消费者:8.81662ms(均摊 2.938µs/次)
8 个消费者:36.407338ms(均摊 12.135µs/次)→ >4 倍判据
与该次提交的内容无关(改动都在 agent/core、memory、qq 插件级别与
scheduler 日志),是"按耗时比值"的判据在共享 runner + -race 下抖动。
改法(不削弱拦截能力):
- 常规构建阈值 4 倍(本机实测 1.1~1.5 倍);
- -race 构建阈值 6 倍(影子内存检查放大常数因子);
- 两者都低于 O(N) 广播的 8 倍量级,真出现线性恶化仍会被抓。
新增 race_off.go / race_on.go 提供 raceEnabled 常量(build tag 判定),
而不是用环境变量或时间门槛(后者要么本机永远跳过、要么 CI 永远误报)。
刻意**没有**改成"重复测量取最小值":实测会让本用例成本 ×3,-race 下
撞 600s 超时(试过,已撤回)。
|
2026-10-27 17:49:05 +08:00 |
|
|
|
0f98f36e71
|
fix(memory): L1 恢复为"工作记忆区 = 时序对话",工具结果展开、仅超长折叠
用户裁定的 L1 契约(2026-10-27):
· 保留 m 条(core.agent.max_context_size)、保护最新 n 条
(core.agent.context.protected_count)、其余按相似度淘汰 → 我的上一版把
m 写到了不存在的键(core.agent.context.max_context_size),运行时仍读
core.agent.max_context_size=200 ⇒ L1 堆到 119 条,工作记忆退化成历史堆。
· 送给模型的是**时序排列的对话** → 曾被渲染成"摘要 + 工具管道"
("—/ → 调用工具: a, b → 已回复…",助手回复还截 120 字),
指代消解因此失效(用户回"新乡",接不上"帮我找一套月租<400…")。
· 工具结果在 context 中**展开全文**,只有**超长**结果折叠成句柄
(toolresult_store + toolresult_read 按区间读回)→ 曾额外加 160/400 字、
每事件 3 条的截断,等于把已展开的结果又藏回去。
本次改动(仅渲染层,不动淘汰/归档/中断语义):
· formatMergedTimeline 渲染 [ts] 用户: … / [ts] 助手: …(回复全文)
· 中断轮的"用户原话"取自 qq_get_message/qq_get_history 的回执正文
(Input 是注入样板,不是人话)
· timelineToolLines:普通结果全文展开;ToolResultStores 命中的只给句柄
· 预算估算按实际渲染内容计(用户行 + 助手全文 + 工具行)
判据:internal/agent/core/timeline_contract_test.go
(时序对话/原话来源/全文展开/仅超长折叠)
|
2026-10-27 16:24:09 +08:00 |
|
|
|
279b61198c
|
fix(scheduler): 空闲时入队不再误报 "level insufficient"
现场:日志满屏
[agent] interrupt queued: task#2 class=interrupt level=3 from qq (L3)
vs <nil> (?) — level insufficient
而实际成因是"当前没有运行任务可抢占",与级别毫无关系。这类假因会
把运维引到"是不是级别配低了"的错误方向(本仓反复吃亏的静默/误导同族)。
改成四种成因分开写:
idle (no running task; queued for next pick) / running in critical section
/ preempt cooldown / level insufficient
|
2026-10-27 15:58:55 +08:00 |
|
|
|
9ffdebbb0b
|
fix(agent): 续写占位不再把「承诺后没做的事」一并劝退
现场(用户原话:"那你去啊?说完就停了"):模型经 output_send__qq 说了一句
"我这就去办",内核判定本批全是输出通道发送(ReplyOnly)后补的占位只有终止许可
——"若你的回复已完成,直接返回纯文本即可结束本轮,无需再调用任何工具"——
模型照做返回纯文本,本轮结束,活没干。
但这条护栏不能简单删掉:注释里记着一次事故,通用「请继续」被读成
"再发一条消息",生产实测单轮 34 次 output_send__qq、持续 514 秒。
改法:把两种授权分开说清楚,护栏不削弱。
- 没做完的操作 → 现在继续做完(继续指**非发送类**工具)
- 真的做完了 → 返回纯文本收尾,并明确「不要再发第二条消息」
判据:continuation_placeholder_test.go(必须同时含"继续做完"与
"不要再发第二条消息";不得再出现"无需再调用任何工具")
|
2026-10-27 15:53:49 +08:00 |
|
|
|
20cf138d3b
|
fix(memory): 把插件的 NoMemory 声明落到事件上
声明链此前断在最后一步:
InjectOptions.NoMemory / ChannelDef.NoMemory
→ payload["no_memory"] → stageCtx.NoMemory ✅ 已合成
→ ContextEvent.NoMemory ❌ 从未赋值
后果:QQ 那句取件样板("来自「X」的私聊消息(message_id=…) 先用
qq_get_message 取正文…")每轮都进计算层(向量化/关键词/蒸馏),
而它是指令不是内容 —— 真实对话内容在记忆里被它挤薄。
按字段**既有契约**处置(context.go 的注释写明):只影响计算层,
归档照常。理由是同一条注释里的 2026-10-22 教训 —— coldcontext_query
的 Consume 已从 L2 删除原文,回填事件必须能落回 L2,否则三层全无。
判据:nomemory_flag_test.go
(NoMemory 事件 textForVector 为空但 Input/Response 保留;普通事件不受影响)
|
2026-10-27 15:40:43 +08:00 |
|
|
|
14daf5b584
|
fix(memory): 超长工具结果在时间线里只渲染句柄(不渲染正文)
用户口径(2026-10-27):渲染工具回执,但**超长的才不渲染** —— 那些结果
已经落盘进工具环(toolresult_store),时间线里只应给「有多少、怎么读」,
正文由 toolresult_read(event_id, offset, limit) 按需取。
为什么不能渲染它的预览:预览是多行文本,塞进历史只会压成"半截结果",
信息量还不如一个句柄;而模型真正需要的是"知道有这份东西、从哪儿读"。
判据:TestTimelineOversizedResultRendersHandleOnly
(落盘结果不出现预览正文、必须含 toolresult_read 与 event_id;未落盘回执照旧渲染)
|
2026-10-27 15:27:35 +08:00 |
|
|
|
5deb3eb10d
|
Revert "fix(memory): 中断的取件样板不再写入 L1(颗粒度=单条消息)"
This reverts commit 3a7440a2fe.
|
2026-10-27 15:04:55 +08:00 |
|
|
|
3a7440a2fe
|
fix(memory): 中断的取件样板不再写入 L1(颗粒度=单条消息)
用户口径(反复强调):进记忆的禁忌是**中断消息本身**,不是整轮。
此前 finishInputTask 无条件把 CleanInput 写进事件,而中断轮的 CleanInput 是
"来自「X」的私聊消息(message_id=…) 先用 qq_get_message 取正文…" 这类样板:
每轮一字不差堆进 L1,真实对话内容只能靠工具回执碎片体现 ——
实测用户回一句 "233719",模型反问"那串数字什么意思"(指代消解失效)。
改法:只清样板,不清内容。
- isInjectionBoilerplate 识别插件取件指令("来自「…」…message_id=" 或 "先用…取正文"),
命中则 Input 置空;
- 本轮的工具回执(qq_get_message 正文,NoMemory=false)、agent 回复照常入库;
- 内核中断正文不清空:测试 D1=B 要求「中断任务的上下文加载回被抢占任务之上」。
判据:internal/agent/core/interrupt_input_memory_test.go(样板被清、内核中断保留、
普通输入不受影响、回执与回复保留)
|
2026-10-27 15:02:57 +08:00 |
|
|
|
b2938ed4a7
|
fix(memory): 场景召回按块向量余弦排序 + 召回 query 可见性 + 对话回执放宽
① 场景召回是每轮常驻注入,此前按「场景权重→时间倒序」排,与 query 无关:
老图库的 rt/rtchild/inputch 等历史事实常年霸榜,实测 1833 字符里九成是它们,
刚聊的群聊内容一句没有 —— 这就是"忘事"的机制。
块里本来就存着向量,相关性就是一次余弦比较:RecallByScene 带出端点/原句块
向量,Indexer 按 max(cos) 排序并设 0.20 下限,取前 8。
无稠密空间时保持原顺序(不能用字面匹配冒充语义 —— 既有判据要求
「措辞与记忆零字面重合时场景路必须兜住」)。
② recall query 可见性:cleaner 缺失/生效/回空三种情况都打日志。此前静默回退
原文,"召回 query 其实是工具原始回执"这件事在日志里完全看不到。
实测 qq_get_message: 199 -> 27 bytes(cleaner 生效)。
③ 对话正文类工具(get_message/get_history/list_chats)时间线渲染放宽到 400 字
(160 字会把群聊上下文切成碎片)。
判据:internal/memory/scene_relevance_test.go
|
2026-10-27 13:25:25 +08:00 |
|
|
|
bd066a627a
|
feat(systemd): 服务单元改名 homeagent.service → hologramagent.service
部署目录早已迁到 /home/hologramagent,单元名是最后一处旧名。
UnitName 是唯一来源(internal/systemd/sync.go),deploy-plan.sh 同步。
|
2026-10-27 13:04:25 +08:00 |
|
|
|
e6b6cd12e3
|
fix(memory): 中心向量过期改为异步节流重建(此前永久失效)
现场:'centroid invalidated: samples 1391 -> store 1877' 每轮召回刷一行。
过期分支只打印然后 return nil,false,nil,而重建入口只存在于三个 CLI 工具里
⇒ 块数一旦越过 20% 线就永远过期,中心化校正永久关闭。
修法:过期时登记一次后台重建(每 fingerprint 10 分钟一次,延迟 2s 等读锁释放)。
不能在锁内就地重建 —— LoadCentroid 全程持 RLock,RebuildCentroid/SaveCentroid
需要写锁,就地调用必然自死锁(实测 internal/memory 测试 180s 超时)。
|
2026-10-27 13:04:25 +08:00 |
|
|
|
1dc32be736
|
fix(memory): 召回可用性——L2 片段带正文 + WebUI 上下文走双路
① L2 预填充此前只给摘要行('已回复老大:原来是这个谐音梗'),模型读得懂
'我回复过'却不知道梗是什么 ⇒ 召回命中仍答不知道。现取两段候选
(事件文本块 + 工具回执),JSON 回执先提取字符串值再压成单行。
② WebUI「上下文」页只显示 L3,而 L3 有 72h 冷化延迟 ⇒ 冷化没跑起来时
面板永远'无上下文',看着像召回彻底坏了(实际 L2 一直有命中)。
现 IndexerAPI 带上 L2 片段(与模型预填充同一 QueryScored),前端两路并列。
判据:internal/agent/core/crossmodal_body_test.go
|
2026-10-27 13:04:19 +08:00 |
|
|
|
238f7b5c0b
|
style: gofmt 归位(CI 之外的漂移)
|
2026-10-27 12:18:27 +08:00 |
|
|
|
051247e10f
|
fix(memory): 时间线按声明渲染工具回执 + 能力判定改为配置层
时间线此前只写「→ 调用工具: a, b」,工具取回的正文下一轮彻底消失:
模型记得自己查过,却不记得查到了什么(刚说的话转头就忘)。
ToolDef 上的 Cleaner / NoMemory 声明一直存在,缺的是消费端。
- formatMergedTimeline 渲染 e.ToolResults:Cleaner 优先,NoMemory 工具跳过,
单条 160 字、每事件最多 3 条(+N more),预算估算同步计入
- 模态能力改由配置层声明(ProviderManager.SetCapabilities),
不再靠 provider.(ModalProvider) 断言穿过 adapter 与安全装饰器
- a2a 通道 RecallPolicy none→auto(NoMemory 仍 true:可读不写)
|
2026-10-27 12:16:06 +08:00 |
|
|
|
4af07d08fe
|
fix(memory,llm): L2 预填充并入召回主路 + 回退换 provider 时换 model(视觉失效真因)
## 1. L2(冷上下文)预填充此前只走「注入文本」,等于没有
设计:prune 与 recall 是一对 —— 工具声明 ContextPolicy=prune + RecallPolicy=auto
时,工具输出既触发裁剪、也触发预填充(memoryPass → recallTextFor)。
但 L2 那一路只写在 buildSystemPrompt 里,query 用的是**注入文本**;
QQ 注入的是「来自「X」的私聊消息(message_id=…),先用 qq_get_message 取正文」,
与用户真正说的话毫无相似度 ⇒ 冷上下文预填充恒为空。
(现场:头天晚上讲的冷笑话,第二天早上完全唤不回;14 小时里
memory recall (input…) 只有 5 次、且 query 是元信息。)
改法:把 L2 检索并入 buildMemoryContext —— 输入路用输入文本、工具路用工具正文,
两条路共用同一次召回与预算;buildSystemPrompt 里那份重复注入删掉。
中断消息仍然 NoMemory + RecallPolicy=none(不裁剪、不召回、不进记忆),
这是设计要求,未改动。
## 2. 回退换 provider 时必须换 model(视觉失效的真因)
现场日志:
deepseek 402 Insufficient Balance
→ 回退 llmsproxy,但 req 仍带 model=「deepseek-flash」
→ llmsproxy 403 model_not_allowed
→ llmsproxy 被整源标记 unavailable(冷却)
→ 视觉回退 resolveModalFallback 跳过它 ⇒「配置了视觉却说没有视觉」
两处修正:
· 每个 provider 用自己的 model 发请求(model 是 provider 的属性)
· 403 区分「模型不被该 key 允许」与「凭证失效」:前者只否一个 model,
provider 仍可用于其他 model 与视觉回退
判据:internal/agent/core/provider_model_fallback_test.go
|
2026-10-27 11:49:26 +08:00 |
|
|
|
981b969f58
|
feat(sdk): 受限事件总线的内核实现 + clawhubadapter 改用它
- internal/sdk/events.go:实现 pubsdk.EventsAPI
· publishAllowed:白名单精确匹配 或 本插件命名空间 plugin.<name>.<后缀>
· subscribeAllowed:只开只读事件(tools_ready/security_ready/device_ready)
· 载荷出向经 security.PrepareOutbound(明文→@{别名}),入向经 ScrubInbound
· 键数上限 64;越界**报错**不静默
· Source 由 SDK 固定成插件名(插件不能冒名)
- PluginSDK.Events() 访问器
- clawhubadapter 的 skill_detected 改走 s.Events().Publish,不再 import internal/events
⇒ 它为外部化的唯一阻塞项(事件发布)已消除
判据(internal/sdk/events_test.go,6 条):
白名单放行 / 越界拒绝(raw_input、agent_output、update_request、stage、system、
别的插件命名空间、空类型)/ 自己命名空间放行 / 订阅越界拒绝 / 载荷凭据被替换成别名 / 键数上限
|
2026-10-27 11:16:40 +08:00 |
|
|
|
ffffb94005
|
docs(architecture): 补上「模块化内核」正篇,原描述停在 v1.x
原架构文档对内核的描述仍是旧的单体启动口径(eventLoop select / interceptLoop /
StageHost 细节),完全没有 2026-10 之后的内核装配机制。新增两节:
## 内核装配(模块化内核)
- internal/kernelmod 注册表 + 每模块一个文件夹 + all/ 唯一 import 清单
- Module 接口:Requires 语义是「我需要谁已就绪」而不是「排在谁之后」
- kernelboot.Build() 分层(缺设施/重名/环三类结构性错误带设施清单报死)
+ Run() 逐层并行 Init、每层前复核设施到位
- KernelState 是唯一公共数据结构:类型化字段、拼错即编译错误、同层并行不加锁
- 当前真实 6 层拓扑(20 模块),并说明 update 为何在 L0、security 为何在 L1、
device 为何在 L5
- Group 0 是副作用顺序,不进图但必须最先跑完
- depgraph.bin:依赖图编译期生成 + EmbeddedGraphHash 摘要校验
- 设施名是插件契约(与 sdk.Facility* 逐字一致,拼错即永不启动)
## 认知层(Agent 运行时)
明确后续 eventLoop / RelevanceContext / StageHost 几节属于装配完成后的执行层,
不是内核启动机制 —— 避免读者把两者混为一谈。
顺带:两份文档共 46 处无语言标记的代码块补上 text。
|
2026-10-27 10:09:35 +08:00 |
|
|
|
633143b15a
|
docs(knowledge): 知识库条目同步新术语(L1/L2/L3 + stablemem)
assets/knowledge/homeagent_architecture/content.md 是**知识库数据**(会被
knowledge_search 检索),此前仍写「文档记忆(Document)、图记忆(Graph)」,
与现行对外语义不一致。
改为:上下文记忆(L1,热)/ 冷上下文(L2,cold context)/ 稳定记忆(L3,stablemem)。
至此非归档文档中 doc_query/doc_commit/图记忆 已清零;剩余旧词只在
6 份已加归档说明的历史计划与 scripts/capability-bench/PLAN.md(评测记录)。
|
2026-10-27 10:06:27 +08:00 |
|
|
|
1d52ba85d5
|
docs: 术语与架构措辞对齐(coldcontext / stablemem / security+device 模块)
1. 术语统一:
- doc_query / doc_commit → coldcontext_query / coldcontext_commit
- 「图记忆 / Graph 层」→「稳定记忆 / Stablemem 层(L3)」
- 分层命名补上 L1/L2/L3 语义:Context(热) / Cold-context(冷) / Stablemem(持久)
涉及 README.md、README_EN.md、assets/docs/{zh,en}/{ARCHITECTURE,OVERVIEW}.md
2. 补上本次最大的结构变化(文档里此前完全没有):
- 安全防火墙作为**独立内核模块**:凭据闸门、工具黑白名单+沙箱、
敏感词与提示词注入预警、按主体授权与插件命名限式;显式代理而非 monkey patch
- device 数据面作为**独立内核模块**:工具注册与路由、inputch/outputch、设备登记;
路由表在 internal/toolroute 以避免 core ↔ kernelboot 成环
3. 历史计划文档(memory-*.md / context-overflow-l4-design / entities-retirement /
git-branching)加**归档说明**:明确术语已过时、现行口径以 ARCHITECTURE.md 为准。
刻意不改写历史正文 —— 那是决策记录,改写等于伪造。
剩余旧词只出现在归档文档与 scripts/capability-bench/PLAN.md(评测历史记录)。
|
2026-10-27 09:58:14 +08:00 |
|
|
|
c33199cc5a
|
fix(webui,plugin): 内核控制按钮晚绑定 + 按钮布局;plugin_start 报错给候选名
1. 内核控制通道晚绑定(真机 503 实测)
POST /api/v1/kernel/restart 一直返回 503:webui 由 plugin-host 在装配期
拉起,而 main() 里 SetKernelControl 还没执行 ⇒ 插件 Start 拿到 nil。
依赖图保证不了 main 里的语句顺序,所以改成让插件拿**解析器**
(lazyKernelControl),调用那一刻再取真实通道;授权仍在每次调用时
按 kernelControlAllowed/kernelRestartAllowed 复核,不因晚绑定放宽。
2. WebUI 重启/关机按钮布局
原先用 .theme-btn(顶栏图标样式:pill + margin-right + 无文字)混在
侧栏底部的文字按钮里 ⇒ 大小不一、挤在一起、关机看不出是危险操作。
改为与「退出登录」同族的 btn-sm,带图标+文字,关机用 danger 悬停。
3. plugin_start 失败报错
原来只说加载出错,或插件不存在,一个可用名字都不给,模型只能盲猜
(实测把 files 写成 file)。现在给:失败原因分类(目录不存在/没清单/
自身加载失败)、最接近的名字(Levenshtein ≤2)、已安装清单(最多 12)。
4. scripts/migrate-plugins-to-org.sh:插件迁到 GitHub 组织独立仓的脚本收口
测试:go build ./... 通过;go test ./internal/plugin ./internal/plugins/webui 全绿。
|
2026-10-27 01:43:19 +08:00 |
|
|
|
55fa2685ae
|
feat(security,device): security 成为独立内核模块(闸门+策略+限式),device 接管内置工具路由
security(新模块 internal/kernelmod/security)
- 凭据闸门:出向拦截成 @{别名}、入向清洗、执行前回填(internal/security/{gate,runtime}.go)
- 工具黑白名单 + 沙箱(读写根、网络、.. 逃逸):internal/security/policy.go
- 敏感词过滤 + 提示词注入预警(Finding 不带原文):internal/security/content.go
- 按主体授权(hard-private 对非内部主体永久拒绝):internal/security/principal.go
- 插件命名限式(可选档位,插件声明/内核执行,只能收窄):internal/security/profile.go
- 启动装配从 cmd/homed 迁入模块;启动顺序判据改盯真实装配点
device(新模块)
- 正式内核模块,Provides FacDevice,KernelState.Device
- 内置工具路由表下沉到 internal/toolroute(避免 core↔kernelboot 环),
device 保留对外入口;agent 未挂载时退化本地 switch
- Agent 侧只留 api.ToolHost/DeviceSurface 适配层
SDK
- InjectOptions.SecurityProfile:插件按 inputch 声明本轮安全档位(可选)
- InjectOptions.TurnHint:插件注入**本轮事实**,内核以独立 system 消息注入,
不混进用户正文(人设负责怎么说话,TurnHint 只放这一轮成立的事实)
- FacilitySecurity / FacilityDevice
提示词
- 内核自带 prompt/skill/工具返回全面英文化(中文提示会让模型行为漂移)
- 机制解释下沉到 4 个内置 skill(memory/output/tool-order/plugins),
skill 描述压到 80 runes 内(原来 94–108 全被索引截断)
- 默认人格模板英文化;[Voice] 段删除,规则并入 persona 块(单一段落)
- 历史记忆工具改名 coldcontext_query/commit,图记忆语义改称 stable memory
修复
- tracker: exec mount → unix.Mount(降权运行 + AmbientCapabilities 下才能挂)、
区分 EPERM 与不支持;启动日志说明后果与恢复方式
- healthcheck_kernel 输出瘦身:status.Tools 只给摘要(原 98,989 tokens/151% 预算)
- plugin_list 描述讲清 auto_start(按需)与 alive(此刻是否在跑),
明确不能由 alive=false 推断能力不存在
- pluginmgr/webui 去掉写死的 /home/newqqagent 回退路径(env → pluginDir 反推 → 报错)
- lua: 盘上适配器与内置版本不一致且无基线时响亮告警(deepseek 流式丢失
tool_calls 导致'模型不调工具'的真因)
测试:go build ./... 通过;go test ./... 60 包全绿
|
2026-10-26 20:55:25 +08:00 |
|
|
|
ca956f69bf
|
fix(update): 升级后第一次启动被误回滚(真机 OTA 实测)+ 关闭期 nil 解引用
## 一、★★★ 升级永远无法生效(rollback.go)
真机跑 OTA 闭环时暴露:
-apply 输出:✅ 升级完成:1.4.0dev (commit ca5b1b1)
重启后日志:ROLLED BACK kernel 1.4.0dev → 1.4.0dev(上一次升级未能确认启动)
原因:pending 标记是在**替换之前**写的(这是对的,见下),所以升级后的
**第一次启动必然看到它**。而原实现"只要标记在就回滚",从不比对当前运行的
是哪个版本 ⇒ 每次升级都在第一次启动时被自己撤销。
正确语义(两段式确认的本意)是比对版本:
当前版本 == 标记里的 To ⇒ 新版本**已经跑起来了** ⇒ 确认、清标记、继续
当前版本 != To(或读不到) ⇒ 新版本没能成为运行版本 ⇒ 回滚到备份
标记仍然必须写在替换**之前**:反过来(替换后才写)会在"替换成功但没来得及
写标记就崩了"时丢标记,那时就不知道该回滚了。
两条判据(用真能执行 `--version` 的脚本做假二进制 —— 原来那些用例写死字节、
根本读不出版本,只覆盖了"读不到 ⇒ 回滚"这一条分支,所以抓不到本缺陷):
TestRollback_ConfirmsWhenRunningTargetVersion 跑的是 To ⇒ 不回滚 + 清标记
TestRollback_RollsBackWhenRunningOldVersion 跑的是 From ⇒ 必须回滚
## 二、每次重启必现两条 panic(proc/host.go)
[stage] tool xxx handler panic: invalid memory address or nil pointer dereference
github.com/.../proc.(*unifiedRegion).generation(...)
关机/重启时 SharedRef 统一段先被拆掉,而在途的工具调用(stage handler /
cleaner proxy)仍会走到 Host.Generation() ⇒ 解引用 nil。panic 虽被 recover,
但把"正常关闭"伪装成故障,并刷屏掩盖真问题。加 nil 守卫(连 h 一起判)。
## 三、下载客户端 20s 总超时(release.go)
`DownloadLatestRelease` 复用了查询用的 kernelHTTPClient(Timeout=20s,
那是**整条请求**上限,含读完 body)。而发行包近 900MB ⇒ 必然
`context deadline exceeded ... while reading body`,"从 Release 自动更新"
永久失败,且错误看起来像网络问题。
改为独立下载客户端:建连/TLS/响应头仍分层设超时(卡住能失败),
读 body 不设总时长。
|
2026-10-26 14:03:05 +08:00 |
|
|
|
ca5b1b17b7
|
test(pluginmgr): 判据钉住暴露给 agent 的两个更新工具
kernel_update / plugin_update 是**模型唯一能触达的更新入口**。它们被删掉、
改名、或参数对不上时,agent 就再也升不了级 —— 而源码里 grep 得到工具名
不能证明它注册成功(本仓 E-3 教训:源码有、运行时没有)。
三条判据,全部走真实 registerTools(捕获 sdk.SDKConfig.RegTool):
1. 两个工具都注册了,且 Serial=true
更新会替换文件并重启,与 install/remove 并发会撞车。
2. kernel_update 同时暴露两种来源
缺 pkg ⇒ 只能用 Release;缺 release ⇒ 只能用本机包。
描述还必须说清"二选一",否则模型会都传或都不传,白烧一轮往返。
3. plugin_update 必须有显式范围
缺 plugins 参数就无法表达范围;默认全升会让用户"只升一个"变成批量重装。
|
2026-10-26 13:48:39 +08:00 |
|
|
|
817f6c59ae
|
fix(update): 重启交还 guard、OTA 支持 release/包两种来源、修三处真 bug
## 一、重启不再由 update 自己 systemctl(用户口径:应该通知 guard)
原 internal/update 直接 `systemctl restart homeagent`。三个问题:
① 杀掉整个 cgroup(unit 里 KillMode=control-group)⇒ **guard 跟着死**,
而 guard 存在的意义就是不让自己死。
② 丢失 exit 42 语义:guard 的 failures/failbackRound/failbackDone 全丢。
③ 自毁竞态:发命令的进程正是被重启杀掉的进程 ⇒ CombinedOutput 可能报错,
**一次成功的升级被误判失败而回滚**。
现在 internal/update 只到"产物就绪":预检 → 备份 → 写 pending → 原子替换。
重启由调用方走**既有**通道(restartCh → exit 42 → guard 用新二进制重拉):
· webui h.kernelCtl.RequestKernelRestart(本来就有,且是白名单内)
· pluginmgr p.sdk.KernelControl().RequestKernelRestart(见下面第三条)
· CLI 外部管理命令,restart unit 后按**版本自报**核对是否真生效
## 二、OTA 支持两种来源(release 自动更新 / 指定安装包)
新增 internal/update/source.go:
Source{Kind: FromPackage, Path} → 直接用本地包
Source{Kind: FromRelease} → 查最新 Release → 下载 → 校验 digest
ParseSource(pkg, release) 统一判空规则(pkg 优先),避免三端各写一套。
两条来源解析后进**同一条** Apply,不存在两份升级实现。
## 三、修三处已提交代码里的真 bug
① **HEAD 编译不过**:d17e261 提交了 kernelmod/update/mod.go(引用 ks.Update),
但 kernelboot/kernelstate.go 的字段改动从未提交。干净 worktree 实测
`ks.Update undefined` —— 推上去的仓库是坏的。本次补上。
② **kernelDataDir 算错一级**:Dir(Dir("/home/newqqagent/plugins")) = "/home",
而第 0 层 update 模块从 ks.DataDir=/home/newqqagent 读 pending
⇒ **agent 发起的升级一旦启动失败就永不回滚**(静默失效)。改为 Dir 一次。
③ **pluginmgr 的 KernelControl 恒为 nil**:它不在 kernelControlAllowed 里,
于是 `if kc := p.sdk.KernelControl(); kc != nil` 永不成立 ⇒
换完二进制**从不重启** ⇒ pending 留着 ⇒ 下次重启被判"未确认"而回滚。
修法**不是**把 pluginmgr 加进完整白名单 —— 那等于让 LLM 能关机整个服务
(kernel_control_gate_test 明确拦这个)。而是拆出**仅重启**授权:
kernelControlAllowed = {webui} 完整(含关机)
kernelRestartAllowed = {pluginmgr} 仅重启(shutdown 直接报错)
两者的危险程度差一个量级:重启几秒后 guard 自己拉回来,关机不会自己回来。
## 四、EventBus:插件请求更新不必 import internal/update
新增 events.EventUpdateRequest,pluginmgr 订阅并受理:
scope=kernel → kernelUpdateApply;scope=plugin → pluginUpdate(apply)。
它在 internal/plugins/webui 的 SSE 守卫里显式登记为**内核内部控制事件**
(不该推给浏览器:前端要的是 update_progress/update_result,不是"谁发了请求")。
## 五、顺手修掉的静默失败
· webui /kernel/update 原实现:**先**写 202 Accepted,**再**解析 body,
之后每个错误都直接 `return` —— 用户看到"已受理"然后什么都没发生。
改为同步 Apply(失败可如实报 HTTP 状态),成功后才请求重启。
· pluginmgr 拿不到控制通道时不再假装成功,而是回报 applied=true + 明确原因
(否则用户以为已升级,进程却还在跑旧镜像)。
· update 事件 publishResult 的 errMsg 原先是死代码(`_ = errMsg`),
按 payload 约定写回 error 字段。
|
2026-10-26 13:45:54 +08:00 |
|
|
|
6dc87965ad
|
feat(update): support release and local package OTA sources
|
2026-10-26 11:16:38 +08:00 |
|
|
|
5b759e80d7
|
feat(update): 更新进度走 EventBus + SSE(穿越进程重启的通知通道)
## 为什么必须走事件总线(用户口径:为什么不走 eventbus)
升级是**长耗时且会让进程消失**的动作。此前进度只往 log.Printf 写,
WebUI 要看得自己轮询 /kernel/update —— 而轮询在重启瞬间必然断线,
断线那一刻恰恰是用户最需要知道"升到哪一步/失败了/已回滚"的时刻。
EventBus 是内核既有的发布订阅机制(同步投递、handler panic 隔离),
用它不需要新造一条通知通道。
## 三个新事件类型
update_check 检查结果(只读,不改动)
update_progress 升级步骤:verify/backup/replace/restart/rollback/done/failed
update_result 最终结果(含回滚轨迹)
## ★ 顺序约束:先发事件,再动作
升级尾部几步(replace → restart)会让进程消失。顺序一反,最后几步的
事件永远发不出去。所以每个 publish* 都紧贴在其描述的动作**之前**。
## 直接 import events,不搞接口垫层
第一版我写了 EventPublisher 接口 + map[string]interface{} payload 想
"避免依赖",那是自欺欺人:events 包**不依赖任何内部包**(已验证),
import 它不会成环;而垫层会把事件降级成无类型 map,丢掉编译期检查。
已回退该设计,改为直接传 *events.Bus。
## SDK 补 EventBus() getter
PluginSDK 原先只有 Publish/Subscribe,没有暴露总线对象。而长流程
(升级闭环)需要把总线本身传下去持续发事件 —— 只给 Publish 的话,
每个调用点都要手动转发一遍。
## SSE 通路(守卫判据逼出来的正确修法)
TestEveryPublishedEventHasAnOutwardPath 抓到三个新事件"有发布者但无对外
通路"——症状是「发了、没报错、客户端收不到」。修法不是加白名单豁免,
而是真的接进 SSE:三者都进 sseEventRing(不只是实时推送),
断线重连要能补到**最终结论**(update_result),否则用户重连后只看到
"开始升级"而没有结局。
## 调用点
· pluginmgr(kernel_update):p.sdk.EventBus() → DoUpgradeBus
· webui(/kernel/update):h.sdk.EventBus() → runKernelUpgrade(pkg, bus)
· CLI:bus=nil,退化成纯 printf(CLI 没有总线)
|
2026-10-26 09:23:58 +08:00 |
|
|
|
d17e261b76
|
feat(update): update 注册为内核标准模块(第 0 层)+ 启动失败自动回滚
## 为什么必须是第 0 层(用户口径)
update 肩负「启动失败回滚上一次升级」的职责,该职责必须**早于所有模块**。
若排在后面:升级后新内核在 memory/llm 阶段崩了,而回滚还没跑 ⇒ 机器进
崩溃循环,只能靠 guard 的 failback(受限启动)兜底。failback 是最后手段,
不该承担"换错二进制"这种**已知可回滚**的故障。
⇒ Requires() 为空 ⇒ 依赖图天然把它放 Layers[0]。
## 两段式升级确认(rollback.go)
升级成功 ──→ 写 pending 标记("这个版本尚未被确认能启动")
↓
重启 → 内核启动 → update 模块(第 0 层)读标记
↓
┌────────────┴────────────┐
标记还在 标记已不在
(上次升级没能活到清除) (上次升级已确认成功)
↓ ↓
回滚到备份版本 正常继续 + 清标记
清除时机:模块跑完自检后立刻清。只依赖"进程活着且执行到第 0 层",
不依赖其他模块 ⇒ 不会误判。
**为什么不用 TTL / 重启次数**:那是 guard 已有的 failback,但判据是
"崩了几次",不是"这次升级该不该撤销"。用重启次数会在"新版本偶发崩溃"时
误回滚 —— 而那种情况该修的是崩溃,不是回退版本。
## 分层(与仓库其他模块同一惯例)
internal/update/ 业务实现(三端共用:CLI/WebAPI/agent 工具)
internal/kernelmod/update/ 适配层(读 ks → 调业务包 → 写回 ks)
参照 supervisor 模块:实现在 internal/supervisor,模块里只做装配。
分开的理由:internal/update 被三端调用,它们都没有 KernelState。
## 标记写入时机(一个真实的顺序坑)
MarkPendingUpgrade 在**替换二进制之前**调用。反过来(替换后才写)会在
"替换成功但没来得及写标记就崩了"时丢标记 ⇒ 下次启动不知道该回滚。
且写标记失败 ⇒ **放弃本次升级**(未替换任何文件):没有标记就没有回滚
能力,而不带回滚的升级等于赌运气。
## 回滚失败必须响亮
备份不存在时返回 error,调用方(模块 Init)必须中止启动 —— 继续装配等于
用一个已知起不来的版本。错误文案带备份路径与标记路径(删标记即可放弃回滚),
人能照着修。
## 判据
internal/update/rollback_test.go 10 条,2 条经变异验证:
· TestRollback_RestoresBackup —— 二进制+depgraph 同批回滚、标记清除、
坏版本留档 homed-broken-*、版本轨迹记录
变异:回滚后不清标记 ⇒ 红
· TestRollback_MissingBackupIsLoud —— 备份缺失时报错且可执行
变异:静默返回 nil ⇒ 红
· TestPendingMarker_AbsentIsNotError —— 无标记返回 (nil,nil) 而非 error
(否则每次启动都走"回滚失败"分支 ⇒ 内核永远起不来)
· TestPendingMarker_CorruptIsActionable —— 标记损坏时提示可删
· TestStateSummary_* —— 回滚信息进 healthcheck;nil 安全
## 顺带修正的既有判据
· runtime_dep_test.go:L0 设施集合从 2 个变 3 个(新增 kernel.version),
改为断言**集合**而非顺序(注册表本就是乱序的)
· facilities.go:新增 FacVersion,并登记为 internalOnlyFacilities
(它是启动时序事实,不是插件契约 —— facilities_sync_test 的守卫正确拦住了
"把内部标记泄漏成对外契约")
三个 DoUpgrade 调用点补 dataDir 参数(标记落点必须与内核 --data 一致)。
|
2026-10-26 02:08:29 +08:00 |
|
|
|
475aac867b
|
refactor(update): 内核与插件更新合并为 update 子系统(三端共享一个底层)
## 为什么合并(用户口径:更新部分应独立为子系统)
之前 kernelupgrade / pluginupdate 两个包,但它们共享三样东西,
分开写就要各复制一份,然后慢慢不一致:
① **版本比较** —— 合并前共 6 个函数做同一件事:
kernel 侧 ParseSemver / CmpSemver / versionNewer / trimTagV
plugin 侧 trimV / isNewer / semverNums(还自带一份正则)
且**行为不一致**:kernel 侧只挡 len<2,"1.4" 漏过去被填成 1.4.0;
plugin 侧的正则要求两段所以也漏。两份都把 "1.4" 当合法版本。
② **两阶段交互**(检查 → 确认 → 执行)—— 同一套用户口径。
③ **失败要响亮** —— 都不静默降级。
## 收敛结果
internal/update/
doc.go 子系统头:职责边界、三端共享契约、为何不分家
semver.go ParseSemver / CmpSemver / Newer / TrimTag(**唯一一份**)
kernel.go 内核升级闭环(解包→校验→备份→替换→重启→复核→回滚)
release.go GitHub Release 查询
plugin.go 插件升级(查仓库→拉源码→hmapdev 编译→产物)
三端共享:
agent 工具(plugin_update / kernel_update) ┐
WebAPI(/plugins/update、/kernel/update) ├─ internal/update
CLI(homed-upgrade) ┘
入口层只做参数解析与结果呈现,不含业务逻辑 —— 否则会出现
"agent 说没有新版、WebUI 却升了级"的分裂状态。
## 合并时抓到的两个真实 bug
① **ParseSemver 只挡 len<2** ⇒ "1.4" 被零值填成 1.4.0,
「1.4 → 1.4.0」被判完全相同(说"无需升级"),而两者内容未必相同。
现在要求恰好三段。
② **不认 v 前缀** ⇒ GitHub release tag 全是 v1.4.0 形态,
CLI 对"当前 v1.4.0 vs 包内 v1.4.0"判不可比、只能退回字符串比较。
现在 TrimTag 去 v 前缀后再比。
## 新增入口
· plugin_update(agent 工具)+ GET/POST /plugins/update
—— 两阶段:apply=false 只检查(列出 updatable / no_repo 两类,
后者是"未声明仓库、无法自动更新"的内置/私有插件);
apply=true 需显式 plugins=all 或名单(不默认全升)
· kernel_update(agent 工具)+ GET/POST /kernel/update
—— 两阶段同构;apply 只收本机包路径(URL 下载是 CLI 职责,
agent 不该在推理中发起长下载)
判据:
· internal/update: Newer / TrimTag / ParseSemver 的边界(含上述两个 bug 的回归)
· pluginmgr 入口层 5 条:检查形状、apply 必须显式给范围、
kernel apply 必须给 pkg 且指向 homed-upgrade、拒绝目录当包
|
2026-10-26 01:05:09 +08:00 |
|
|
|
02022bc13d
|
feat(ota): kernelupgrade 抽成独立包 + 插件仓库迁移脚本
## 1. internal/kernelupgrade:抽包(避免两份实现漂移)
原来 641 行全在 cmd/homed-upgrade 的 main 包里。内核侧的 kernel_update
工具要复用同一套逻辑,而 main 包不可被 import —— 照抄一份的后果是
"CLI 能升级、agent 调用却把机器搞坏"。
导出:ReadBinVersion / CompareVersions / ParseSemver / CmpSemver /
ProbePluginCompat / ExtractPkg / FindFile / DoUpgrade / WaitHealthy 等。
CLI 变薄壳(只留 flag 解析与 fail 输出)。
★ 顺带修一个真实 bug(判据抓到):ParseSemver 原来只挡 len<2,
于是 "1.4" 漏过去、缺失的 patch 被零值填成 1.4.0 —— 会让
「1.4 → 1.4.0」被判成完全相同(进而说"无需升级"),而两者内容未必相同。
现在要求恰好三段。
## 2. scripts/migrate-plugins-to-org.sh:插件迁成独立仓库
为什么迁:插件散在 SDK 仓 example/ 下有三个问题 ——
① 插件版本与 SDK 版本绑死(一次 SDK 发版把所有插件都带上)
② 插件作者不能单独为自己的插件发版
③ plugin_update 需要"仓库地址 → 拉源码 → 编译",而 example/ 是
别人的仓库,不是插件自己的仓库
脚本纪律:
· 默认 --dry-run,不碰 GitHub
· 产物不入库(dist/build/rawfile/node_modules/*.hmap 全排除)——
实测源码 3MB、产物 1.9G,后者进了仓库就再也删不干净
· 幂等:仓库已存在(422)则跳过创建,重跑安全
· 自动给 plg.json 补 repo 字段,使 plugin_update 能定位仓库
· 用 tar 管道而非 rsync:后者在最小化系统里常常没有(本机实测就没有)
执行结果:26 个插件全部迁移(25 Go + 1 Lua),仓库名与插件名同名。
|
2026-10-26 00:06:59 +08:00 |
|
|
|
17f36ac430
|
Merge pull request #29 from JianFeeeee/merge/id10-closeout
fix(plugin)+ci: KERNEL-REGISTRY-SETLOCK 全量收尾(Set* 加锁 / 撤 race 排除项 / 门禁空表守卫 / 撤证据步②)
|
2026-10-25 22:48:07 +08:00 |
|
|
|
29150065aa
|
fix(clawhub): sidecar 就绪超时 5s → 30s(慢机器上 node 冷启动超时)
CI 报红:`TestLaunchSidecarAndListTools`
launch sidecar: sidecar echoplugin ping timeout (5.00s)
本机全绿。
## 这不是"测试机器慢所以放宽测试"
sidecar 起不来等于该 skill 整体不可用 —— 这是**产品行为**,不是测试细节。
原来的 5s 是按开发机估算的:node 冷启动 + 模块加载在快盘空载时约 0.2s。
而 CI 共享 runner(以及真实部署里的低配机器)上,一次 fork+exec+node
引导遇到 CPU 争抢就会超过 5s。
判断"真的挂了"应该由**进程退出信号**负责(readLoop 会感知),
而不是由引导耗时猜。
## 30s 的取法
· 比 systemd 的 TimeoutStopSec(25s) 宽 —— 关停时不该被就绪等待拖住
· 与用户可感知的"卡住"阈值(约 30s)持平 —— 再长就该让用户看到明确失败
· 错误文案带上超时值与可能原因(node 不可用),便于自助排查
|
2026-10-25 22:31:51 +08:00 |
|
|
|
23ed997b04
|
test(proc): Publish 延迟判据改为自校准噪声基准 —— 修 CI 误报
CI 报红:`TestStreaming_PublishLatencyFlatAcrossSubscribers`
1 个消费者:6.16615ms(均摊 2.055µs/次)
8 个消费者:30.466442ms(均摊 10.155µs/次)
→ 比值 4.9 越界(阈值 4),报"疑似线性恶化"
而本机连跑 3 次全绿。
## 根因:不是被测逻辑,是分母量级 + 单次采样的抖动
单次 Publish 真实成本只有 0.4~2µs。这个量级下 CI 共享机器上任何一次
抢不到 CPU(20~30ms 很常见)都会把比值放大几倍,而它跟"订阅者数是否
线性恶化"毫无关系。原判据各只测一次,一次抖动就定生死。
## 为什么不用绝对时间门槛(我先试过,是错的方向)
本机 423ns/次、CI 2µs/次,量级差 5 倍。任何固定阈值要么在本机永远
跳过(判据失效、等于删掉测试),要么在 CI 永远误报。
## 改为自校准:同配置两段之差即噪声水平
`measure` 内部把 rounds 拆成前后两段,分别计时。同一配置、同一消费者、
同一机器——两段之间的差异只可能来自环境抖动,与机器快慢无关。
· 两段差异 > 2 倍 ⇒ 环境不可测,跳过比值判定(只记录)
· 否则照旧判比值 > 4 倍 ⇒ 红
## 为什么不把 measure 调两次(也是我先试过、错的方向)
`EvtConsumer.Stop()` 只关自己的 stop channel,而 Run 正阻塞在
`evtfd.Read`——后续没有 notify 唤醒就不会返回,`wg.Wait` 于是永久
挂住。实测连续调用直接把测试拖到 240s/360s 超时。
分段测量在**同一次 consumer 生命周期内**完成,不碰这个问题。
## 判据仍有识别能力
变异验证:在 8 订阅者的计时区间内人为加入 7 倍写开销(模拟 O(N) 广播),
比值会达到 8 倍量级,远超 2 倍噪声门与 4 倍阈值 ⇒ 仍然判红。
|
2026-10-25 22:28:22 +08:00 |
|