|
|
cc07adb20b
|
style(webui): dashboard.js 格式化(换行与引号风格)
编辑器/格式化器在保存时改的,只动换行与引号,无语义变化:
· 一行多条件的 if 拆成多行
· 含双引号的字符串改用双引号包裹(避免转义)
· 长字符串拼接拆分
13 insertions / 4 deletions,逐行核过无逻辑改动。
|
2026-10-31 16:20:47 +08:00 |
|
|
|
47cdb03918
|
merge: pr31 update 回滚安全
|
2026-10-31 15:56:11 +08:00 |
|
|
|
f9ee58a914
|
merge: pr31 — update 回滚安全(原子写 + 回读校验),解 19 处冲突
pr31 与 main 是**双向分叉**,不是单向落后。`ba281a8` 已把它的多数工作
在 main 上重新实现(dataDir 守卫、gc.go、dataDir 解析),但两边各自又
往前走了,所以 6 个文件 19 处冲突要逐条判。
## 合进来的(main 缺)
- **标记原子写**:main 是 `os.WriteFile` 直接覆写,掉电留半截标记;
改为 `写 .tmp → fsync → close → rename → fsync 目录`
- **H4**:`publishResult` 里 `if errMsg != "" { _ = errMsg }` 把失败原因
丢了,订阅方只看到 ok=false。现在 error 字段恒定存在
- **回读校验补最低可用性**:main 对无 digest 的旧 marker 只比"内容与备份
一致",而**零字节备份内容也一致** ⇒ 判成回滚成功、不清 marker 的时机
也错过了。补"非空 + 带可执行位",刻意**不**要求 `--version`
(老内核没这个 flag,那正是 PR #31 的回归)
- `kernel_apply_test.go`(836 行,含 B2c `TestMarkPendingUpgrade_PreservesOldMarkerOnFailure`)
## 保留 main 的(pr31 在这几处是旧版或更差)
- **回滚校验用 digest**(`verifyRestored`)而不是 `--version`:
pr31 要求回滚后能读版本,而 1.3.x 及以前没有 `--version`,
而它们恰恰是最常见的回滚目标 ⇒ 从旧版升级失败后永久回滚不了
- **`restoreDepGraph` 三态**:原来有+备份有→还原、原来有+备份无→不算成功、
原来无→删掉新装的。pr31 只处理第一种,会导致「旧 homed + 新 depgraph」
的崩溃循环
- **marker 在还原完整后才清**:pr31 在还原前就清,还原失败即失去第二次自愈
- **CLI 不重启服务**:pr31 仍带 `restartUnit`,会杀掉整个 cgroup 连带 guard
- **`resolveDataDir`**:pr31 的 `upgradeDataDir` 是它的子集,多扫几个候选前缀
## 冲突处理中的三处重复声明
两边各加了同名函数,自动合并把它们都留下了:
- `readBinVersionStrict` —— 删 pr31 的(main 在 77 行有)
- `kernelDataDirMu/kernelDataDir/SetKernelDataDir/KernelDataDir` —— 删 kernel.go
里那份(main 在 install.go)
- `newPkgRoot` —— pr31 的重命名 `newPkgRootFromBin`,两套测试各自可用
- rollback.go 里 `verifyRestored` 与 `readBinVersionStrict` 两段校验并存 —— 删后者
顺带修掉删除时误伤的 `ShortSum` 注释,并删掉 pr31 引入但无人调用的
`DataDirEnv`(main 明确不再拿 env 当 dataDir 来源)。
## 验证
go build ./... ✓
go vet ./... ✓
go test ./... -count=1 62 ok / 0 fail
go test -race ./internal/update/ ... ✓
gofmt -l 干净
|
2026-10-31 15:56:10 +08:00 |
|
|
|
58dea7e6c4
|
merge: 介绍站内容修正
|
2026-10-31 11:44:04 +08:00 |
|
|
|
aa9e8c7454
|
docs(site): 修正介绍站三处与代码不符的描述
## 1. 「一轮里的多个 tool_call 默认并发」写反了
同一张卡的标题是「同轮工具默认串行,声明后才并发」,正文却写「默认并发」。
按 `internal/agent/core/argvalidate.go:307` 的 `batchRunnable`:批内**每一个**
工具都 `ParallelSafe` 才并发,有一个没声明就整批串行,`Serial` 优先级更高。
改:标题保留,正文按实现重写。
## 2. 「最近 10 条恒受保护」已不成立
`effectiveProtectedCount` 现在返回 `min(配置值, ContextTokens/单条平均token)`。
固定 10 条在 50k 窗口、5800 token/事件时会占 58k,比 40000 预算还大 —— 这正是
召回率从 44% 掉到 22% 的那次实测。站点两处都还在写「恒受保护」,改成「受预算约束」。
## 3. 第二层命名与描述
- 「低分下沉成文档,冷文档再蒸馏」→ 冷上下文
- ② 层正文「JSON + TF-IDF 索引的中间层」→ 实际是稠密 + TF-IDF 双路,
且与 L1 结构同构、取回走两阶段
- FAQ「上下文事件裁剪后归档成文档」同步改
全站 `文档层|冷文档|归档成文档|Document 层` 归零。
HTML 结构校验:div / section 配对差 0,`<p>` 开闭 62/62。
|
2026-10-31 11:44:04 +08:00 |
|
|
|
8a6e480175
|
merge: 架构节改参考文档写法
|
2026-10-31 11:42:23 +08:00 |
|
|
|
bd9dd9e75f
|
docs: 架构节改成参考文档写法
上一版还是论证体:四个小标题里三个是否定式断言(「上下文**不是**截断+摘要」
「三层记忆是迁移,**不是**复制」「记忆按场面分,**不按**会话」),
每段都以一句评论收尾。段落字数 62–128,几乎等长 —— 这是最明显的机器痕迹。
改成参考文档:
## 小标题换成名词
上下文不是「截断 + 摘要」 → 上下文窗口
三层记忆是迁移,不是复制 → 三层记忆
记忆按场面分,不按会话 → 场面
标题说明这节讲什么,不下判断。判断该由正文的数据支撑,不该写在标题里。
## 删掉与假想敌的对比
「常见做法是把超窗的对话裁掉或让模型自己摘要,两者的共同问题是丢什么由位置决定」
「普通 agent 通常一条会话一条历史,换个话题就失忆」
换成直接说我们怎么做的。读者不需要先被告知别人怎么错。
## 段落长度拉开
之前 62–128 字(极差 66)。现在每节先给一行文件路径,正文长短不一:
32 字 `internal/agent/core/context.go`
127 字 这条约束来自一次实测。50k 窗口……
51 字 N 默认 10,实际取值受预算约束:……
129 字 场面由五个特征构成……
极差 98 字。技术文档本来就是「一行路径 + 一段解释 + 一行数字」这种不均匀节奏。
## 去掉两处破折号
「没有『取出即删除』的路径 —— 删除排在确认之后」
「不吸收整轮指纹 —— 吸收了会……」
改成句号。全文破折号归零。
## 补上实现位置
原来只讲设计没给代码位置,改成每节标文件路径:
`internal/agent/core/context.go`、`internal/memory/coldcontext/`、
`internal/memory/scene_emerge.go`、`internal/agent/core/stages.go`。
## 核对
文中数字逐条对源码:`preemptPromotionCap = 2`、`minSceneEvidence = 2`、
`FindColdContexts(72*time.Hour, 2)` 均一致。两版 9 个标题对齐,无死链。
|
2026-10-31 11:42:21 +08:00 |
|
|
|
e542bc1643
|
merge: 介绍站命名同步
|
2026-10-31 11:41:18 +08:00 |
|
|
|
27f25bdc4f
|
docs(site): 介绍站第二层改名 Cold Context
与仓库内文档同步:第二层早已改名 cold context(工具名 coldcontext_query /
coldcontext_commit),站点仍在用「Document 层」,且第二层描述写的是
「JSON + TF-IDF 索引」—— 实际是稠密向量与 TF-IDF 双路检索。
改了 6 处:meta description、三层配色注释、① 层说明里的归档目标、
② 层标题与正文、三层链路串。② 的正文同时换成当前事实:
与 L1 结构同构、稠密+TF-IDF 双路、取回走「先确认再删」两阶段。
|
2026-10-31 11:41:18 +08:00 |
|
|
|
4df8c73555
|
merge: 第二层改名 Cold Context + 许可段精简
|
2026-10-31 11:39:15 +08:00 |
|
|
|
7c0eec1b2f
|
docs: 第二层改名 Cold Context,清理四组已不存在的符号名
## 问题
README 与两份 ARCHITECTURE / OVERVIEW 仍在用旧称「Document 层」,
而第二层早已改名 cold context(工具名 `coldcontext_query` / `coldcontext_commit`)。
不止是层名。文档里引用的四个符号在代码里**一个都不存在**:
| 文档里写的 | 代码里实际 |
|---|---|
| `docStore.ContextToDoc` | `coldcontextStore.ContextToColdContext` |
| `FindColdDocs` | `FindColdContexts` |
| `docToTriples` | `coldDocToTriples` |
| `docStore.SetVectorizer` | (已无此调用,embedder 构造期注入) |
四处都用 `grep -rn "\b名字\b" --include=*.go` 确认过:旧名 0 处,新名各自 4 处。
## 改了什么
- `assets/docs/zh/ARCHITECTURE.md` 13 处、`en/ARCHITECTURE.md` 15 处
- `assets/docs/zh|en/OVERVIEW.md` 各 3 处
- README 两版:「文档层」→「冷上下文层」
- `assets/branding/mascot-xiaozhai-guide.md` 的丝带配色表(三色分别对应三层)
## 顺带修正层号
ARCHITECTURE 用的是 **L1 Context / L2 Cold Context / L3 Stable**,
我上一版 README 写成了 L0/L1/L2 —— 少了一层,且与架构文档对不上。已改为 L1/L2/L3。
## 顺带修掉断掉的交叉引用
`internal/agent/core/toolcall.go` 与 `recallcoldcontext_test.go` 的注释写
「ARCHITECTURE「记忆流转」② Document 层的读取分支」,指的正是刚改名的节。
不改就成了悬空引用。三个测试函数名(`TestContextToDoc` 等)指向已删函数,一并改名。
## 验证
- 全仓 `grep` 旧名:`*.md` / `*.go` 均 0 处(排除 build/third_party/.pi)
- `go build ./...` ✓
- `go test ./internal/memory/coldcontext/ ./internal/agent/core/` ✓
|
2026-10-31 11:39:13 +08:00 |
|
|
|
6a2c907e04
|
merge: 架构节补上下文管理与记忆
|
2026-10-31 11:30:22 +08:00 |
|
|
|
152dc680ad
|
docs: 架构节写清上下文管理与记忆的实际做法
## 补上真正的差异点
之前架构节只有一张图和七阶段表,把最核心的上下文管理省略了。
用 codegraph 查了 `RelevanceContext` / `PruneWithProtected` /
`coldcontext.Store` / `block_recall`,确认实际做法,逐条对照常见 agent:
### 上下文:按相关度取舍,不是截断或摘要
常见做法是超窗裁掉旧对话,或让模型自己摘要 —— 共同问题是**丢什么由位置决定**。
这里是给每条历史事件按当前输入打分,低分下沉到文档层。
打分主路径是稠密余弦(与媒体、文档同一向量空间),模型未就绪退到 TF-IDF。
关键在 `effectiveProtectedCount`:保护条数受预算约束。
```go
budgetCap = ContextTokens / avgEventTokens
pCount = min(配置值, budgetCap)
```
源码里记着这条约束的来历:50k 窗口下单条事件约 5800 token(工具回灌型),
预算 40000,「钉住 10 条」本身就装不下 —— 每轮裁剪都撞保护下限,
两轮后预算耗尽,任务直接终止,召回率从 44% 掉到 22%。
### 三层是迁移,不是复制
`ColdDoc` 的注释写得很清楚:「块的真实归属在 Events[i].Blocks 里」,
块在任一层只有一个持有者,不靠引用计数保活。
回灌用 `Peek` + 确认 + `Remove` 两阶段,源码明确标注「**禁止先删后灌**
—— 删除必须排在确认之后,回灌失败即永久丢数据」。
### 场面:两条路并行,且互不吞并
声明场景**不吸收整轮指纹**(`EnsureScene` 的注释):否则它会以接近 1.0 的相似度
吃掉后续所有同类轮次,被动聚类再也长不出更细的场面。
这条边界是踩出来的,不是设计时想到的。
## 开头相应收缩
原来开头四段「**粗体断言** + 三行解释」与架构节重复。改成一句介绍 + 安装 +
三段话点出差异,细节留给架构节。
两版 163 / 191 行,9 个标题对齐,代码块 10/10 配对,无死链。
|
2026-10-31 11:30:18 +08:00 |
|
|
|
5f570075a3
|
docs: 补回被误删的架构节,许可段压缩,去掉重复
## 上一轮我把架构整节删掉了
用 `s.index()` 替换区间时,把「架构」到「下载」之间的内容一起吞了。
README 里「架构」出现 0 次 —— 最该详细写的部分没了。已补回:
- 内核/插件三层通道图(ASCII,按 CJK 宽度对齐到 39 列)
- 七阶段表(`on_input` … `after_output` 各自时机与插件能做什么)
- 内循环规则与三个退出条件
- 三层记忆与插件崩溃退避(1s/2s/3s,5 分钟内第 4 次停自动重启)
## 许可段 16 行 → 7 行
原文逐条拆:
| 原文 | 问题 |
|---|---|
| 「GPL 家族里**传染性最强**的一档」 | 比喻+最高级,许可段不需要修辞 |
| 「即:任何人把改过的……」 | 重复上一句,只是换了说法 |
| 「边界很清楚:」 | 过渡铺垫 |
| 「子进程隔离在这里不重要了」 | 反驳没人提的问题(humanizer §A.5) |
| 「第三方插件生态的安全与活跃正建立在这条之上」 | 纯空话 |
用户只需要知道三件事:改过的版本对外提供服务要开源;自己写的插件不受约束、
可以闭源商用;第三方组件保留原许可。
## 去掉重复与模板化开头
- 「内核不发 HTTP、不读文件」在架构段与「其余」里各出现一次,删掉后一处
- 开头原本连续四段 `**粗体断言。** + 三行解释` —— 同一个模板重复四遍就是排比。
改成一句话开场 + 安装命令,设计细节另起 `## 几个设计`
- 全文破折号从 1 处降到 0
## 验证
- 两版链接无死链,代码块配对(6/6)
- 6 个二级标题中英对齐
- ASCII 图逐行按 east_asian_width 计算,框线全部 39 列
|
2026-10-31 11:29:24 +08:00 |
|
|
|
7b4c6ead7f
|
merge: README 写真正的独到设计
|
2026-10-31 11:27:20 +08:00 |
|
|
|
8a3151c285
|
docs: README 开头改成真正的独到设计
上一版开头是「接上 QQ、WebUI、命令行、文件、浏览器,跑几个月不用重启」——
这不是设计,是所有 agent 框架都能说的话。用 codegraph 查了实际实现,
真正独到的是四条,每条都有可验证的常量与实测值:
## 1. 启动顺序编译期定序
`internal/kernelboot/kernelboot.go` 的 `Module` 接口声明 `Requires`/`Provides`,
构建时 `homed-depgraph` 算出 6 层依赖图写入 `depgraph.bin`,并把 digest 用
`-ldflags` 注入二进制。运行期 `loadStartupPlan` 只读图。
关键在 `CheckAgainstBinary`:图与二进制 digest 不一致时**拒绝启动**,
不猜哪边对(注释原话:"猜一个是拿启动正确性赌运气")。
`Run` 里还有一层:`produced` 是**执行期事实**而非声明推演,`verifyLayerDependencies`
拿它校验,所以咬得住"层顺序被改坏"(实测 lua 排到 llm 之后 ⇒ llm 拿 0x0 LuaVM,SIGSEGV)。
## 2. 插件按设施就绪提前启动
`cmd/homed/kmod_runtime.go` 的 `BeforeRun` 钩子让插件调度器在**执行任何模块之前**
订阅 syncbus,`OnLayerDone` 在每层完成后重新评估拉起。
不是"内核装完再拉插件":声明 `requires_facilities: [config]` 的插件在第 1 层
完成时就起,不必等第 4 层 agent(实测 35 秒 = dense backfill 750 事件 + 重建索引)。
## 3. 两级输入、四级中断、饥饿防护
`internal/agent/core/scheduler.go`:排队输入无级别(任何中断可打断),中断带 L1–L4。
`effectiveLevel = min(L4, Level + min(PreemptCount, 2))` —— 抢占能提级但封顶两档,
`preemptPromotionCap = 2`,防低优先级饿死。
L4 内核独占,插件声明 L4 被 `clampPluginLevel` 夹到 L3。
## 4. 场面涌现
`internal/memory/scene_emerge.go`:指纹由 chan/peer/tool/topic/part 构成,
权重 1.0/1.0/0.8/0.4/0.2。`minSceneEvidence = 2` —— 只发生一次的不建场景。
`joinSceneThreshold = 0.5` vs `recallSceneThreshold = 0.35`,
**唤起比归属宽松**(注释:"想不起来是损失,多想起一条只是多几行上下文")。
QQ、WebUI、命令行这些搬到「其余」一节,不再当卖点。
## 验证
- `go run ./cmd/homed-depgraph -print` 确认 20 模块 6 层
- 四个常量在源码里逐一 grep 确认存在且值一致
- 两版各 98 行,4 节结构对齐,链接无死链
|
2026-10-31 11:27:16 +08:00 |
|
|
|
674cbac36c
|
merge: README 重写为入口文档
|
2026-10-31 11:24:00 +08:00 |
|
|
|
52100dfa77
|
docs: README 改成 README 的样子
前三版都写成了演讲稿:一上来三段议论讲设计理念,用"它就在这些地方干活"
这种拟人句,把架构取舍当成卖点铺在正文里。420 行里真正有用的入口信息
(安装、命令、能力清单、文档链接)不到三成。
去看了几个热门项目的 README 怎么写的:
- **ollama**:`Start building with open models.` 一句,然后就是 macOS /
Windows / Linux / Docker 四个安装块,接着 `ollama run gemma4`
- **caddy**:`Caddy is an extensible server platform that uses TLS by default.`
一句,然后徽章、安装、命令
- **lazygit**:赞助商 + 截图 + 安装
共同点:**没有设计理念**。设计写进 docs 或 blog,README 只负责让人五分钟内
把东西跑起来。也没有"它"这个人称,直接说功能。
## 现在的结构
```
自托管 Agent 内核。接上 QQ、WebUI、命令行、文件、浏览器,跑几个月不用重启,还记得住事。
make build build-cli / ./build/homed / ./build/waiter
API 密钥在 WebUI 里填。需要 Linux。
## 能做什么 五行清单
## 架构 一张图 + 三段话
## 下载 三变体表格
## 构建
## 许可
```
100 行。原来 420 行。
删掉的内容去哪了:
- 设计取舍(为什么用子进程、`.so` 踩过什么坑)→ 已有一句压在架构段
末尾,完整版在 `docs/zh/kernel-secure-firewall-design.md` 等设计文档里
- 长篇能力叙述 → 五行清单,细节在 SDK 文档
## 顺带修正
- "`internal/plugins/` 下 13 个内置插件" → 实际 18 个插件目录,改为不写具体数字
- 中文版重复的 `## 快速体验` / `## 文档`,英文版两个 `## Documentation`,
合并后各留一处
## 验证
- 两版死链检查为空
- 代码块配对(6 / 8)
- 两版 5 节结构对齐
英文版同步,不是直译。
|
2026-10-31 11:23:56 +08:00 |
|
|
|
9fe82c8e26
|
merge: 介绍站标题与链接修正
|
2026-10-31 11:20:41 +08:00 |
|
|
|
9363660b79
|
docs(site): 改掉格言式小标题,修 21 个 404 链接,校正统计数字
## 小标题:格言改成陈述
原站的小标题全是这个调子:
```
崩一个,不赔上全部 拦得住,也接得回
三条道,各走各的 数据如水,随流,随改,随走
轻重缓急,各归其位 忙时,有人替你接住
部署,从未如此便捷
```
这是「句子很响,但读者看完不知道这节讲什么」。正文其实写得很扎实
(有退避 1s/2s/3s、L4 被夹到 L3、60 万 token 上限这些实测值),
只有标题在唱口号。
改成陈述式,读者扫一眼就知道这节讲什么:
```
插件崩溃不影响内核 中断可以抢占,也能恢复现场
三条通信通道各管一类数据 大块数据走共享内存,不序列化
输入分两类,中断分四级 积压的消息由驻留子 agent 接手
部署是拷一个文件
```
另外两处本来就重复:「三条道,各走各的」和「数据如水,随流,随改,随走」
讲的是同一件事(都讲共享内存)。前者改成只讲三通道分工,后者保留细节。
## 21 个 404 链接
「可用插件」里每个插件的源码链接都指向
`github.com/JianFeeeee/hologramagentsdk/blob/main/example/<name>`,
而 `example/` 下的 26 个目录已迁走并删除,**全部 404**。
改为 `github.com/HologramAgent/<name>`,逐仓 `git ls-remote` 验证过都存在。
徽章右侧的可见文字也从 `SDK / example/qq` 改成 `GitHub / HologramAgent/qq`。
## 统计数字虚高
按 `site/README.md` 自己的核对流程实测(生产日志
`homed_2026-10-31_10-05-16.log`):
| 页面项 | 原值 | 实测 |
|---|---|---|
| 在跑插件 | 36 | **30** |
| 可用工具 | 340 | **285** |
| Go 代码行 | 109k | **240k** |
| LLM 适配器 | 9 | 9 ✓ |
Go 行数是**反向错**的:核对命令没排除 `build/`,而 `build/` 里打包了 SDK 副本,
不排除会得到 516549 行,排除后 240056 行。已把该排除项写进 `site/README.md`
的核对命令,避免下次再算错。
数字校正清单也写进 `site/README.md`,标注核对日期与日志文件。
## 验证
- HTML 标签配对:div / article / a 全部为 0 差
- 行数未变(3417),改动集中在标题与链接
|
2026-10-31 11:20:37 +08:00 |
|
|
|
49e80307c7
|
merge: README 去 AI 味(humanizer-zh 规则)
|
2026-10-31 11:15:36 +08:00 |
|
|
|
28b78e1703
|
docs: 按 humanizer-zh 规则再改 README 开头
上一版开头四句被指出仍有 AI 味。拆开看问题在:
· 四句长度接近、每句都是金句,没有一句是普通陈述
· "说人话:"是元话语,AI 特有的"宣布要通俗化"动作
· "白天聊过的事,晚上还记得" + "三周前说过的偏好,不会因为上下文窗口
滚动就丢了" —— 严格对仗,一个口语一个技术解释混在一起
· "插件崩了它不倒,模型换了它不改" —— 两组四字对仗
用 humanizer-zh 的规则过了一遍,命中三条:
1. **§A.1 不是 X,而是 Y** —— "跟直接调 API 的区别在于它把长期当成了
基本需求,而不是事后加个向量库",后半句是假想敌。删掉后逻辑没损失。
2. **§A.2 单句收尾** —— "这个仓库从 2026 年 7 月开始开发,现在 1.4.0,
一千多个提交" 独立成段作收尾。并入上一段。
3. **§A.5 与假想敌辩论** —— 小标题 "它跟别的 Agent 框架哪里不一样"。
改成 "几个设计决定"。
另外自重写:
- 开头从四句金句改成一句陈述 + 一句具体 + 一段解释。不是排比。
- "## 能力" 从五项 "**类别**:三词·三词·三词" 改成有详有略的散文
(五项同模板是最密集的 AI 指纹之一)
- "## 几个设计决定" 从五条 "**X,不是 Y。**" 改成一段段长短不一的叙述,
其中 3 条原本是同一个句式
- 删掉与开头重复的记忆下沉描述
英文版同步,不是直译:`What makes it different` → `Design decisions`,
开头去掉同样的假想敌与收尾句。
## 修掉一处我自己写出来的事实错误
上一版我写了 "微信和 QQ 的文件、语音消息能收发"。核查后:仓库里没有微信
插件,只有 clawhubadapter 注释里提到微信作为通用通道的例子。已删掉微信。
## 验证
- 两版死链检查为空
- 代码块配对(14/14)
- 9 个二级标题中英对齐
|
2026-10-31 11:15:33 +08:00 |
|
|
|
8c72bd8ec5
|
merge: README 重写(中英对齐 + 历史移入 CHANGELOG)
|
2026-10-31 11:08:23 +08:00 |
|
|
|
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 |
|
|
|
86d1f4c547
|
test(update): 补 TestMarkPendingUpgrade_PreservesOldMarkerOnFailure(判据 B2c)
## 为什么还要再加一条
前两条 commit 之后,B2 的「原子写」在判据侧其实是哑的 —— 我复核时实测
过:把 MarkPendingUpgrade 的 `tmp := p + ".tmp"` 改成 `tmp := p`
(退回最初的原地覆写 bug),以及把整个函数退回 os.WriteFile,
`TestMarkPendingUpgrade_IsAtomic` **都照样绿**。
因为它断言的是「最终状态对」:不留 .tmp 残留、内容可解析、可覆写。
os.WriteFile 这三条全都满足。也就是说那条用例证明的是「结果正确」,
没证明「写的过程不会毁掉已有数据」—— 而后者才是 B2 要防的事故。
## 这条用例钉什么
用它逼出两者唯一可观测的差异:**失败路径上旧文件还在不在**。
① 先写一个完好的旧 marker(FromVer=1.0.0)
② 把 pendingPath+".tmp" 占成**目录** ⇒ OpenFile 必 EISDIR
(稳定可复现,不依赖权限/磁盘/平台;失败点与「磁盘满/掉电」同类:
已打开目标、还没写完全部内容)
③ MarkPendingUpgrade 必须返回 error(不能假装标记已就绪)
④ 旧 marker 必须原样保留:FromVer 仍为 1.0.0、ToVer 仍为 1.1.0
③④ 都用 Error 而非 Fatal:让两个症状一起报出来。
## 语义差异
原子写(现状):新内容写进 .tmp 时失败 ⇒ 目标文件仍是完整旧版本
原地覆写 :O_TRUNC 先清空再写,中途失败 ⇒ 目标文件被截断,
旧 marker 永久丢失 ⇒ 不知道上次升了什么,也没了回滚依据
## 变异(已实测)
B2c-M1 rollback.go `tmp := p + ".tmp"` → `tmp := p` ⇒ RED
实际输出:旧 marker 被破坏了:FromVer 应为 1.0.0,实际 "2.0.0"
零用例假绿也堵住了:判据脚本对 `no tests to run` 直接 FATAL。
仅动 internal/update/kernel_apply_test.go(测试文件),生产代码零改动。
|
2026-10-28 17:56:44 +08:00 |
|
|
|
1319d6619c
|
fix(update): dataDir 取内核权威源 + 回滚可信校验 + marker 原子写
承上一个 commit(回退 85fd38b)。本 commit 是 R2 主体:B0/B1/B2 三项修复,
以及为 P1–P4 补齐的**行为级**判据。
━━━ B0 · dataDir 不再推导,取内核真实 -data ━━━
`internal/update/kernel.go`
· 删除 ResolveDataDir(env → `<exe>/data` 兜底)。
· 新增 SetKernelDataDir / KernelDataDir:值由内核适配层在启动时
用 ks.DataDir 注入。进程内调用方(pluginmgr / webui)直接取,
不做任何推导;取不到返回**空串**。
· DataDirEnv 保留但降级为诊断参考(不再是合法来源)。
`internal/kernelmod/update/mod.go`
· Init 里 upd.SetKernelDataDir(ks.DataDir);为空时打响亮告警
(不阻断启动 —— 第 0 层职责是回滚判定,不是参数校验)。
`internal/plugins/pluginmgr/kernel_update.go`、`internal/plugins/webui/kernel_update.go`
· kernelDataDir / dataDirOf 都改为 return update.KernelDataDir()。
`cmd/homed-upgrade/main.go`
· upgradeDataDir(explicit):① `-data <dir>`;② config.db 的
core.daemon.data_dir(只读连接 + PRAGMA query_only=1,不建表不迁移)。
· 两者都拿不到 ⇒ 返回空串 ⇒ `-apply` 在动手之前就 fail 并说明怎么修。
→ 这样 P1 的空值守卫重新变成**活的**:拿不到真实 dataDir 就真的会拦。
━━━ B1 · 回滚可信:回读校验是唯一判据(判据 B1)━━━
`internal/update/rollback.go` — RollbackIfUnconfirmed
· rollback.go:315 `rb, verr := readBinVersionStrict(binPath)`
替换后**必须**回读成功且 rb.Version == rec.FromVer,否则
rolled=false + error + **不清 marker**(下次启动再试,并让人看到日志)。
CopyFile 返回 nil 只代表字节搬完了,不代表搬过去的东西能执行。
· rollback.go:254 确认分支同样改用 readBinVersionStrict。
这里曾经用 ReadBinVersion —— 它含 InferVersionFromRunning 回退,
已实测:binPath 是不可执行残文件、marker.ToVer 恰好等于本机 waiter
版本时,会从 waiter **借到**版本号 ⇒ confirmed=true ⇒ 清 marker、
不回滚 ⇒ 机器带着跑不起来的二进制继续启动且失去自愈机会。
该缺陷环境相关(CI 没装 homed/waiter 会绿),故判据不能依赖本机环境。
· 备份缺失 / 回读失败 / 版本不符 / depgraph 回滚失败 —— 四种情形
一律不清 marker。
· 新增 readBinVersionStrict(kernel.go):只认「这个文件自己能报出版本」,
无任何回退;ReadBinVersion 改为其包装器(保留探测用回退),
注释写明它**不能**用作回滚校验。
━━━ B2 · marker 原子 + 损坏不卡启动(判据 B2)━━━
`internal/update/rollback.go` — MarkPendingUpgrade
· 旧实现 os.WriteFile 直接覆写(O_TRUNC),掉电留半截 JSON。
· 现改为:写 <path>.tmp → fsync(文件) → rename(原子) → fsync(目录)。
目录 fsync 失败仅告警(容器/tmpfs 下 EINVAL 不该阻断升级)。
rollback.go:112 `if err := f.Sync(); err != nil {` 即这条链路的第一道闸。
· RollbackIfUnconfirmed 遇到解析不了的 marker:quarantineMarker 改名
留存(不删,保留现场)+ 告警 + return false,"","",nil
⇒ 当作「无待确认升级」继续启动。把内核卡在启动阶段是最差选择:
能执行到这行已证明当前二进制可执行。
`internal/kernelmod/update/mod.go`
· 补注释钉住语义:rerr != nil 必须中止启动;但 marker **损坏**不算
失败(已由 RollbackIfUnconfirmed 吞掉并改名留存)。
━━━ 判据补齐(行为级,修掉 M6 真空洞)━━━
`internal/update/kernel_apply_test.go`
· P1 TestApplyRefusesEmptyDataDir 加强:切到 t.TempDir() 并拍快照,
断言拒绝升级时**根本没写任何文件**(旧用例只查「目标二进制没变」,
把守卫挪到预检之后依旧绿 —— 因为 filepath.Join("", prefix) 会把
备份目录建到 CWD)。
· P3 新增 TestApplyPrunesOldBackups:预置 5 个历史备份后跑真实 Apply,
断言旧的被 GC。★ 这是补 M6 的真空洞 —— 旧的变异脚本只跑
`-run TestPruneOldBackups`(只证明函数能跑,3 毫秒),把 Apply 里
那一行 PruneOldBackups(dataDir) 删掉它照样绿;新脚本必须同时跑
TestApplyPrunesOldBackups,删掉调用后立刻报红。
「函数能跑」≠「Apply 真的调了它」。
· B1 新增 TestRollback_RefusesCorruptBackup(零字节/坏备份 ⇒ 必须
回滚失败且不清 marker)、TestRollback_RefusesVersionMismatch
(备份版本 ≠ FromVer ⇒ 同样失败)、
TestRollback_ConfirmedBranchIgnoresInferredVersion 与
TestConfirmedBranch_StrictProbeBitesOnUnparseable(确认分支不得
使用推断值;用可注入的 inferHomedPath/inferWaiterPath 造出
「能借到版本」的场景,不依赖本机是否装了 homed/waiter)。
· B2 新增 TestMarkPendingUpgrade_IsAtomic、
TestRollback_CorruptMarkerDoesNotBlockBoot。
`internal/update/rollback_test.go`
· TestRollback_RestoresBackup / TestRollback_RollsBackWhenRunningOldVersion
的备份从纯字节 "OLD-GOOD" 改为**真正可执行**的假二进制(1.4.0),
并按版本而非按字节断言。旧的写法只能测「字节搬对了」,测不到
「搬过去的东西能跑」—— 而后者才是回滚能救机器的前提。
`internal/update/kernel.go`
· inferHomedPath / inferWaiterPath 提为包级变量(生产默认值不变),
唯一目的是让「确认分支不得使用推断值」这条判据可稳定复现。
━━━ 判据锚点自检(grep -F 命中数均为 1,行号已核对)━━━
P1 kernel.go:348 if dataDir == "" {
P2 kernel.go:370 backup := filepath.Join(dataDir, BackupDirPrefix+ts)
P3 kernel.go:449 PruneOldBackups(dataDir)
P4 kernel.go:445 publishResult(bus, "kernel", "", true, …)
P4 events.go:69 "error": errMsg,
B1 rollback.go:254 if cur, cerr := readBinVersionStrict(binPath); cerr == nil {
B1 rollback.go:315 rb, verr := readBinVersionStrict(binPath)
B2 rollback.go:112 if err := f.Sync(); err != nil {
验证:go build ./... 通过;go test ./internal/update/ 通过。
|
2026-10-28 17:51:52 +08:00 |
|
|
|
a8ecb8257c
|
revert: 回退 85fd38b(ResolveDataDir 从 exe 猜 dataDir,生产必挂)
This reverts commit 85fd38b7d1.
## 为什么回退
85fd38b 把三端 dataDir 推导统一成 update.ResolveDataDir(exePath):
`HOMEAGENT_DATA_DIR` → `<exe 所在目录>/data` 兜底。方向(三端必须算出
同一个值)是对的,但**兜底那一项在生产上是错的**:
ExecStart=/usr/bin/homed --role=guard --data /var/lib/hologram
ResolveDataDir("/usr/bin/homed") == "/usr/bin/data" ← 与真实值不符
`HOMEAGENT_DATA_DIR` 在现有部署里并没有设置,所以实际生效的就是这个兜底。
后果两个,都是致命的:
① Apply 第一步要在 dataDir 下建备份目录,而 /usr/bin 在
ProtectSystem=strict 下只读 ⇒ EROFS ⇒ 升级必挂
② pending 标记落到内核读不到的位置 ⇒ 升级启动失败后静默不回滚
## 附带问题:空值守卫变成死代码
ecde8dd 的 H1(P1)加的是「dataDir 为空就拒绝升级」。85fd38b 的
ResolveDataDir 兜底**永不为空**(exePath 为空时还会自己去取
KernelBinaryPath),于是三个调用点传进来的 dataDir 恒非空 ——
守卫还在,但守的是一个永远非空的常量,实际不会再拦任何东西。
「不知道」伪装成「知道」比没有守卫更危险:守卫看起来在守,出事时才发现
它从来没生效。所以判据 P1 在 85fd38b 之上是咬不住的。
## 回退范围
只回退 85fd38b 的三个文件(CLI / pluginmgr / webui 的推导逻辑),
回到 ecde8dd 的状态;不影响 ecde8dd 本身。回退后:
· ResolveDataDir 及其所有调用点消失,无悬空引用
· go build ./... 通过
· 三端暂时回到「各端自己推导」——这正是紧随其后的下一个 commit
要替换掉的形态(改为取内核权威源,不做任何推导)
回退 + 后续修复合起来才完整:单看本 commit,dataDir 解析又散回三处;
下一个 commit 用「注入内核真实 -data」取代全部推导。
|
2026-10-28 17:51:16 +08:00 |
|
|
|
85fd38b7d1
|
fix(update): 统一 dataDir 推导,三端共用 update.ResolveDataDir
范围说明:本 commit 与 H1 属同一缺陷的另一面。Apply 现在会拒绝空
dataDir,而三端各自的推导逻辑此前会**真的返回空**(或写死生产路径),
不一起改则 H1 会把所有升级都拒掉、或让 pending 落到内核读不到的地方。
三处原来的推导:
· cmd/homed-upgrade/main.go env → 写死 /home/newqqagent
· pluginmgr/kernel_update.go env → Dir(pluginDir)
· webui/kernel_update.go env → 空(由调用方报错)
问题:① 硬编码 /home/newqqagent 在目录/用户迁移后指向不存在的路径;
② 从 pluginDir 反推与 webui 侧不一致,同一台机器两条升级通道算出
两个 dataDir ⇒ 标记落在 A、第 0 层 update 从 B 读 ⇒ 升级启动
失败时静默不回滚(症状是崩溃循环,极难反查到标记)。
现在三端统一调用 update.ResolveDataDir(exePath):
① HOMEAGENT_DATA_DIR(部署方显式声明,唯一可靠来源)
② <exe 所在目录>/data 兜底(替代原先的硬编码生产路径)
并删掉 cmd/homed-upgrade 里写死的 /home/newqqagent。
本 commit 可单独回退:回退后仅剩「各端自己推导」,与 H1 组合会退化为
「空 dataDir 一律拒绝升级」(安全侧失败,不会产生无回滚能力的升级)。
|
2026-10-28 17:20:40 +08:00 |
|
|
|
ecde8dd515
|
fix(update): 补上内核升级缺失的四道安全闸(H1–H4)
四条缺陷的共同点都是「静默产出一个看起来成功的升级」,而失败的表现
是机器进崩溃循环且没人知道原因。
H1 dataDir 为空必须拒绝升级
Apply 此前只在 dataDir 非空时写 pending,空值仅跳过标记、后照常替换
二进制 ⇒ 产出「无回滚能力」的升级。现改为在动任何文件之前直接失败。
另新增导出 update.ResolveDataDir(env → <exe>/data 兜底)。
H2 备份目录不能写 /root
backup 原为 /root/homed-upgrade-backup-<ts>,服务在 ProtectSystem=strict
+ ReadWritePaths=/var/lib/hologram 下运行,非 root 对 /root 无写权限
⇒ MkdirAll 必 EACCES ⇒ 首次升级必挂。改为落在 dataDir 下。
rollback 的 homed-broken-<ts> 同样从(通常只读的)二进制目录挪到 dataDir。
H3 备份目录要有 GC
旧注释写「确认稳定后可删」,但全仓无任何删除逻辑。新增
PruneOldBackups:保留最近 3 个 homed-upgrade-backup-* 与最近 1 个
homed-broken-*,其余 RemoveAll,在 Apply 成功返回前调用。
只处理名字精确为「前缀 + NowStamp 形态」的目录,不误删 pending 标记
或前缀相近的第三方目录。
H4 结果事件吞错误 + 成功不发 result
publishResult 里 `if errMsg != "" { _ = errMsg }` 把失败原因丢弃,payload
也没有 error 字段 ⇒ 订阅方只看到 ok=false。现在 error 字段恒定存在。
Apply 成功路径此前从不发 result,现补发 ok=true。
测试:kernel_apply_test.go 新增 5 个用例(含题目要求的四个函数名)。
|
2026-10-28 17:20:35 +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 |
|