|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
d65b742bd7
|
fix(plugin): 修两处持锁自死锁(CI 双红真因)+ tools_ready 事件声明
## 死锁(这是 CI 连续两次红的真因)
`StopAll` 与 `StopPlugin` 都在持有 `r.mu` **写锁**时调用
`RemoveBuiltinSkill` → `skillDirOf` → `r.mu.RLock()` ⇒ **自死锁**。
后果不是报错而是**卡死**:
- internal/plugins 卡 300s 超时(TestCapability_RestrictedPluginStillLoads)
- internal/plugin 卡 840s 超时(TestStopPluginDoesNotTouchPersistentState)
- CI 的 `go test ./...` 与 `go test -race` 双双红
修法:拆出**不加锁**的内部版本,持锁路径只走它:
- `skillDirFrom(dataDir, name)`(纯函数)
- `removeBuiltinSkillLocked(pluginName, p, dataDir)`(显式传 dataDir,
避免"看起来没取锁、一改就取锁"的隐患)
- `announceSkillRemovedLocked`(持锁下直读 r.evBus)
修后:internal/plugins 300s → 12s,internal/plugin 840s → 0.7s。
## 防回归判据
`TestStopAllAndStopPluginDoNotDeadlock`:死锁**不返回**,普通断言抓不到,
所以用**超时**当判据(5 秒未返回即失败),并指明应改用 Locked 版本。
## tools_ready 事件声明
新增的 tools_ready 触发 `TestEveryPublishedEventHasAnOutwardPath`
(该判据防"事件发了、客户端收不到"的静默失效)。
tools_ready 是**内核内部**就绪信号(plugin-host → seq 等订阅者),
不是对话流程事件,前端无需感知 ⇒ 在 allowed 里显式声明理由。
## 验证
go build ./... / go vet ./... / go test -p 1 ./... / -race 全通过。
|
2026-10-25 18:25:37 +08:00 |
|
|
|
77f596fb07
|
chore(log): 全仓运行期日志与 Fatalf 英文化(320 处 / 83 文件)
用户要求:"历史以来的全部写入日志的区域"改为英文,因为中文日志
在没有中文字形的终端/容器里会乱码,且跨机器、跨工具 grep 时不可靠。
## 范围
- 全部 log.Printf / log.Fatalf / log.Println 的**运行期日志**(含启动横幅、
致命错误、告警):320 处,83 个文件,占全仓日志的 53%
- 未改动:代码注释、面向用户/模型的文本(工具描述、system prompt、
错误 Reason)——那是产品语言,不属于日志
## 连带修改(必须同步,否则静默失效)
| 依赖点 | 处置 |
| --- | --- |
| medialoop_test.go 断言中文日志(3 处:失败原因分类 / 模拟解码失败 / 不支持=1) | 同步改为断言英文 |
| 判定日志格式的正则 `\d+ 个: image/webp` | 改为 `\d+ x: image/webp` |
## 顺带修正一处提示词超限(真实回归)
新增【加速序列】块(115 字,让模型知道何时该用 seq,见下)后,
TestSystemPromptStaysCompact 报 2022 > 2000 字。
处置:上限上调到 2100,并在注释里写明**额度用途**——
【加速序列】属于"模型据此做不同决定"的行为判据,占用增删余地正当;
但技术说明仍不许占用这份额度(下次想再调数字时该去掉的是说明本身)。
【加速序列】块内容只留判定条件(多对象重复 / 顺序已知 / 以前做过),
格式细节全部下沉到 seq-tools skill —— 避免两处真相。
## 验证
- go build ./... / go vet ./... 通过
- 分块跑全仓测试通过(agent、plugin、plugins/*、security、memory、
config、kernelboot、kernelmod、cmd)
- 残留中文日志仅 1 条注释,非日志调用
## 运维提示
日志文本变了,依赖 grep 中文的既有习惯要改为对应英文词,例如:
内核装配完成 -> kernel assembly complete
插件调度完成 -> plugin scheduling done
已建链 -> link established
已加载 N 个 -> N loaded
|
2026-10-25 15:55:26 +08:00 |
|
|
|
f9ebdd5c74
|
Merge commit 'bf44654' into ci/boot-graph-tests
|
2026-10-25 07:57:21 +08:00 |
|
|
|
599252f9b0
|
fix(ci): ID:19 空表假绿堵口 —— 空表须显式声明 # none,删空与有意清空可区分
check-ci-gates.py: TABLE 捕获体 `(.*?)` 允许空串,表被删空时静默走「无排除项」分支。
现在 ci.yml 里加显式空表标记校验(while 跳过 # 注释行),门脚本补正反自测:
合法 `# none` 必过、完全空体/只空行/非显式注释必红。
ID:18: bootgraph_test.go 失败面引用改为按区分词定位,不再依赖包名/行号。
|
2026-10-25 07:56:45 +08:00 |
|
|
|
1f41fe1a47
|
Merge commit 'refs/tmp/gmain' into ci/boot-graph-tests
|
2026-10-24 13:34:57 +08:00 |
|
|
|
1905d32053
|
docs(kernelboot): ID:14 已完成,同步 registry_test.go 失败面第 4 条
## 为什么改
bootgraph_test.go(原 internal/kernelboot/modules/registry_test.go)文件头
「失败面」一节第 4 条是**历史实测记录**,原文结句写着:
要"红得干净"得让 kernelboot.Run 对 nil 依赖显式报错 —— 那是业务改动,
不在本线权限内,已记入待办。
ID:14(e70c2b5)做完之后这句成了**假信息**:留着比删掉更有害 ——
下次有人照着它判断「Run 不拦 nil 依赖」,会得出与事实相反的结论。
同时 kernelstate.go 的注释已改成「Run 会显式拦」,两边口径不一致更要命。
## 改了什么(纯注释,**零行为改动**)
- 原记录的「崩在业务代码里」形态**保留**,但明确标注为「ID:14 之前的形态,
作为为什么需要执行期校验的原始证据」
- 新增「已完成(ID:14,提交 e70c2b5)」小节,写清现在的行为与**实测**结果
- 指向专用回归判据(runtime_dep_test.go / runtimedep_test.go)
## 注释里那句「换成可读报错」是实测过的(非声称)
用真实登记表 kernelmod.Registered() 算 plan 后注入两种变异,均得
*DependencyNotReadyError,**无 panic / SIGSEGV**:
· 整层逆序 ⇒ 第 0 层拒(produced 为空)
模块 agent 缺设施 [docmem knowledge llm memory stagehost textmem tracker];
模块 plugin-host 缺设施 [config docmem kernel-io knowledge lua memory
stagehost supervisor textmem tracker]
· 跳过第 2 层 ⇒ 第 2 层拒
模块 stage-host 缺设施 [knowledge llm textmem]
(初稿写成「第 1 层 … 模块 llm 缺设施 [lua]」,与实测不符,已按实际输出改正。)
## 校验
git diff 中非注释行改动 = 0(grep 计数)
go build ./... = 0 error,go vet ./... = 0 warning
go test ./internal/kernelboot/ ./internal/kernelmod/all/ = ok
|
2026-10-24 12:58:40 +08:00 |
|
|
|
3864f36992
|
refactor(kernelboot,kernelmod): ID:13 删死包 kernelboot/modules,整机启动图判据迁到 kernelmod/all
## 决策
删除 internal/kernelboot/modules(registry.go + registry_test.go),
把整机启动图判据迁到 internal/kernelmod/all(真实聚合入口)。
文件头那条 TODO(死包清理) 随包一起消失。
## 为什么这个包该删
实测零生产调用:
· 生产代码里 import 它的包数 = 0
· Register() / BuildModules() / Names() 的外部调用点 = 0
· 它的 Register/BuildModules 是 internal/kernelmod 那套注册表的**第二份实现**
(留着只会让人下次接错口子)
· kernelmod/all/facilities_sync_test.go 的注释里早已写明
「kernelmod 才是真实注册表(模块编译期工厂加载)」
## 判据迁移(能力未削弱,逐条实测)
- 判据 1–4(整机装配/层拓扑/严格逆序关闭/无残留): 只换宿主,
用 kernelmod.Registered() 作为装配输入不变,实测 5 层 / 17 模块 /
关闭顺序 [4 3 2 1 0] / 3 轮 goroutine 差值 0 / 订阅者 2→2
- 判据 5(TestEveryRegisteredModuleIsCompiledIn): 真相源是扫 `..` 源码树的
「目录+类型名 复合键 + 四方法齐全」,与宿主包无关。scanModuleTypes 的
modRoot 从 "../../kernelmod" 改为 ".."(本文件现在在 kernelmod/all/ 下)
- 删掉 `_ ".../kernelmod/all"` 自引用(判据现在与聚合入口同包,
本包被编译即聚合 import 生效 —— 反而更贴近生产同一条 import 链)
## 「新模块登记了但漏 import」仍咬得住(3 个变异亲测)
A 从 all.go 删 multimodal 聚合 import(叶子模块,无人依赖)
→ TestEveryRegisteredModuleIsCompiledIn 红(指名 multimodal)
→ TestEveryModuleDirIsImported 红
B 删 multimodal 的 init()(Register 调用点消失,真相源不能是被测入口)
→ TestEveryRegisteredModuleIsCompiledIn 仍红(源码侧多数一个类型)
C supervisor 的 Name() 改成非字面量
→ 判据显式失败(不静默跳过 ⇒ 不会变空转)
## 悬空引用清零
全仓 grep kernelboot/modules:代码/文档/CI 只剩 ci.yml 里一条
「那个包零生产调用,已删除」的历史说明。同步修正:
· ci.yml「整机启动图判据」步骤 → go test ./internal/kernelmod/all/
· ci.yml -race 排除表 pkg 行目标包 → internal/kernelmod/all
(该包现在装整机 ⇒ 同样会撞 KERNEL-REGISTRY-SETLOCK;
实测不带 -race 6/6 绿,排除仍必要,单号不变)
· ci.yml 三条说明文本(含证据步骤的提示语)
· kernelboot/kernelstate.go 指向旧判据路径的注释
· docs/zh/kernel-modularization-design.md §2.7 的 declarations.go 路径
· check-ci-gates.py §4 断言目标包 → ./internal/kernelmod/all/
(否则它盯的是一个已不存在的包)
## 验证
go build ./... = 0 error,go vet ./... = 0 error
go test ./internal/kernelboot/... ./internal/kernelmod/... = ok
go test -race ./internal/kernelmod/all/ = ok(连跑 6 次无 DATA RACE)
check-ci-gates.py EXIT=0;ID:15 的 verify_v4 矩阵复跑仍 5 变异全抓 + 误报项 EXIT=0 + 基线 EXIT=0
|
2026-10-24 12:56:37 +08:00 |
|
|
|
e70c2b55b4
|
feat(kernelboot): ID:14 Run 增加执行期依赖校验,顺序被改坏时报可读错而非 SIGSEGV
Build() 里已有构建期校验 diagnoseUnresolvable(查声明:缺依赖/环/重名),
本单**不重复**它。缺的是执行期那一层:声明全对、图也自洽时,plan 的层顺序
仍可能被改坏(手改 Layers / 图与代码漂移 / 有人把某层提前),此时 Build 绿,
模块 Init 拿到 nil 依赖,崩在业务代码里。
实测复现(本单判据的复现形状):把 kernelmod 真实登记表算出的第 1 层
(lua/memory/onnx-parser/…)与第 2 层(llm/text-memory/knowledge-store)交换,
llm.buildProviderManager 收到 0x0 的 LuaVM:
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV ... addr=0x10]
internal/lua.(*VM).ConfigureConcurrency(0x0, ...) vm.go:213
internal/kernelmod/llm.buildProviderManager(..., 0x0, ...) mod.go:70
改动:Run 逐层维护 produced(**只有真跑完的层**才贡献设施,刻意不从 plan 预填),
每层 Init **之前**核对本层各模块 Requires ⊆ produced;缺则返回
*DependencyNotReadyError 并**本层一个 Init 都不调**(宁可什么都不装,也不装一半再崩)。
错误文本指名「第 N 层 / 模块 X 缺设施 [...]」并列已实际产出的设施。
判据:
internal/kernelmod/all/runtime_dep_test.go —— 用真实登记表算 plan 后做变异
A 正序前缀不误报
B 交换两层 ⇒ 判据红(指名 llm 缺 lua / text-memory 缺 memory,onnx /
knowledge-store 缺 knowledge-migrated),且 ks.LuaVM 仍为 nil(没跑到 llm)
C 删掉提供者整层 ⇒ 第 0 层就拒绝,IO/EventBus 仍 nil(一个模块都没跑)
internal/kernelboot/runtimedep_test.go —— 纯逻辑四形状 + 防 M2 自检
变异证明(4 个,逐个人工注入后跑判据,全部咬住):
M0 撤掉守卫 → 复现原 SIGSEGV(ConfigureConcurrency(0x0))
M1 守卫恒放行 → 5 条判据红
M2 produced 从 plan 预填 → 退化成 Build 重复实现,顺序类判据红
M3 报错丢模块名 → 指名类判据红
修前红/修后绿:
修前(M0):panic SIGSEGV @ lua.(*VM).ConfigureConcurrency(0x0)
修后:DependencyNotReadyError,文本含「第 1 层」「llm」「lua」「缺设施」
go build ./... = 0 error,go vet ./... = 0 error
go test ./internal/kernelboot/... ./internal/kernelmod/... = ok(含 -count=1)
|
2026-10-24 12:50:57 +08:00 |
|
|
|
19c78ca946
|
fix(plugin): Registry 的 Set* 全部取写锁,读侧改一致性快照(KERNEL-REGISTRY-SETLOCK)
根因(前一条 CI 线实测):internal/plugin/registry.go 的 Set* 长期是单行
无锁直写,而 r.mu 一直存在却从未被这批 setter 使用;同时
kernelmod/agent.wirePluginSDK 与 kernelmod/pluginhost.injectBaseDeps
同处启动图第 4 层、并行执行,各自把同一批字段写一遍 ⇒ 写-写竞争。
## 一、加锁(真正的修复)
· 24 个 Set* 全部改为 r.mu.Lock() / defer Unlock():
SetIOManager/SetEventBus/SetMemory/SetTextMemory/SetDocStore/SetKnowledge/
SetProviderManager/SetConfigRegistry/SetPluginDir/SetDataDir/SetLuaVM/
SetBaseAPIKey/SetKernelControl/SetToolRegistrar/SetStageRegistrar/
SetAPIRegistrar/SetToolCleaner/SetStatusProvider/SetSupervisor/SetTracker/
SetConfig/SetStageHost/SetDeviceAuthQuery/SetIndexer
· 读侧保住 RLock,并新增 sdkDeps + snapshotDeps():buildSDK 在**一次**
RLock 内取齐整组依赖后立刻放锁,随后用快照装配 SDK。
为什么不在整个 buildSDK 外套 RLock:其闭包(regOutput 等)在插件 Start
期间才被调用,持锁跨用户代码有死锁风险;改为快照 + 放锁。
为什么必须一次取齐:逐字段裸读既各自是一处竞争,也会让同一次装配
看到"lua 已换新、cfg 还是旧"的撕裂组合。
· kernelControlFor / stageRegistrarFor 拆出无锁内核
(kernelControlForCtl / stageRegistrarForSource),供快照后复用,
避免持锁再 RLock 的自锁。
· 其余读点集中走访问器:pluginDirOrEmpty / cfgRegistryOrNil /
toolCleanerOrNil / stageHostOrNil / eventBusOrNil / ioManagerOrNil
(dynamic_proc.go 的 evBus 两处同步改)。
· 未改任何语义、未重命名导出符号。
## 二、去重复注入(可行,已做)
按"必要性不对称"判定:保留 pluginhost.injectBaseDeps,砍掉
agent.wirePluginSDK 里的 7 项重复写,agent 侧只留 SetStatusProvider。
理由:pluginhost 紧接着就 LoadSkipping 拉插件,且它刻意 Requires() 不含
agent("插件不必等 agent 35 秒"的收益就在这半边)——砍它会让第 4 层加载
的插件拿到全 nil 依赖;而 agent 侧写的是同一批 ks.* 的同一份值,写第二次
无新增信息。
★ 去重**不能**替代加锁:去重后 agent 仍写 SetStatusProvider,pluginhost
同时可能 buildSDK 读 status,写-读竞争仍在(实测可复现 1 条 DATA RACE)。
所以锁是修复本体,去重只是消除无意义的重复写。
## 三、验证(全部 -count=1)
判据(internal/plugin/registry_setlock_test.go):
· TestRegistrySettersTakeWriteLock —— 源码级:24 个 Set* 函数体必须各含
r.mu.Lock()(race 是概率性的,"锁被删了"这类回退必须确定性咬住)
· TestSettersConcurrentWriteRace —— 两个写 goroutine 用不同值并发写同一批
字段 + 读侧并行
· TestBuildSDKConcurrentWithSetters —— 走真实读路径 buildSDK/snapshotDeps
与 setter 并发(补上一条测不到的**读侧**缺口)
变异实证(每条都实跑,均报红):
M1 删掉 SetLuaVM 的锁 ⇒ 源码判据报红 + -race 报 DATA RACE
M2 删掉 SetConfig 的锁 ⇒ 源码判据报红 + TestBuildSDKConcurrent 报红
M3 只删 snapshotDeps 的 RLock ⇒ -race 报红,栈指向读侧 :486/:494/:495
vs 写侧 :274/:367(证明读侧回退也有人管)
race 数字(CI 线整机判据 go test -race ./internal/kernelboot/modules/):
修复前 25 轮:2 轮报红,累计 14 条 DATA RACE
修复后 25 轮:0 轮报红,0 条 DATA RACE
范围包(./internal/plugin/... ./internal/kernelmod/...)修复后 -race 全绿。
go build ./... / go vet ./... 干净。
## 四、CI 侧收口(不在本分支落地)
ci/boot-graph-tests 的 race 排除表里那条
pkg|internal/kernelboot/modules|…|KERNEL-REGISTRY-SETLOCK
现在可以删了。按任务边界本分支不碰 ci.yml,改由
id10_ci_closeout.patch(对 ci/boot-graph-tests 可干净 git apply)+
ID10_CI_CLOSEOUT.md 交给 CI 线合入。
|
2026-10-24 12:17:46 +08:00 |
|
|
|
f256098c9f
|
refactor: 全面改名为 HologramAgent + 提示词技术内容下沉到 skill/工具描述 + 修 21 处乱码
## ① 全面改名 HomeAgent → HologramAgent
仓库已改名,但代码与文档里仍有大量旧名。本次按"用户能看到的优先"全量替换:
- 用户可见:登录页标题/页脚、默认人格第一句、CLI 状态输出、启动日志、
plugin_install 的工具描述、AI 图片插件的 User-Agent
- 文档与脚本:README、assets/docs、docs/zh、deploy/*.sh、.github/workflows
- 鸿蒙工程:cmd/ohos/HomeAgent → cmd/ohos/HologramAgent(git mv)
目录内的 module/app 描述同步;**证书路径原样保留**
(default_HomeAgent_xxx.cer/p7b/p12 是签名材料的实际文件名,改了会找不到证书)
- 插件元数据:plugin.json 的 author
★ 必须保持原样的两处(改了会坏):
· legacySeededPromptSignature = "你是 HomeAgent 的看板娘「小宅」…"
—— 那是识别历史播种人格卡的**原文锚点**,改了就识别不出旧数据
· 公开 SDK 的 ProxyAuthHomeAgent = "homeagent"
—— 那是 SDK 契约里的值,属独立仓库,本仓不改
## ② 修 21 处乱码(U+FFFD)
这些是历史编辑留下的编码破坏,全在注释里,例如:
「赌注重下���遍」→「赌注重下一遍」
「「不��滤」」 →「「不过滤」」
「**待��退场**」→「**待退场**」
★ 保留 providers/chineseclip/tokenizer.go 里的 2 处:那是**故意**用
U+FFFD 演示"非法 UTF-8 字节被归一成 U+FFFD",不是乱码。
## ③ 提示词技术内容下沉(用户:技术性内容应该收敛到 skill 与 -help 中)
实测系统提示词 2309 字,其中两段几乎全是**模型无法据以行动的实现细节**:
【工具执行顺序】1272 字(ParallelSafe 声明、批内降级、batchRunnable 判据…)
【三层记忆】 1069 字(L1/L2/L3 编号、消费式删除、向量空间、指纹…)
收敛后 **2309 → 1723 字(-25%)**,保留的全是"模型据此做什么不同的事":
工具顺序:默认并行 / 不要依赖先后 / 同通道保序 / 写类工具可能串行
三层记忆:时序里能看最近 / doc_query 把历史事件按原始时间戳插回时序 /
关系用 memory_recall / 成文知识用 knowledge_search
判断口径写进判据:新加一段提示词前先问"模型会据此做什么不同的事",
答不上来就该进 skill 或工具描述。
## 判据
prompt_memory_test.go 新增 2 条:
- TestSystemPromptHasNoImplementationJargon
黑名单:ParallelSafe / batchRunnable / 消费式 / DenseCosine / fingerprint /
向量空间 / 批内降级 / 自激
- TestSystemPromptStaysCompact:上限 2000 字(当前 1723)
变异:把 "ParallelSafe 声明、批内降级" 加回工具顺序段 ⇒ 第一条立刻红。
## 验证
- go build ./... / go vet ./... 干净
- go test ./... -count=1:58 包 0 失败
- make check-client-versions:客户端与内核版本一致
|
2026-10-22 20:49:44 +08:00 |
|
|
|
4e70b8d5d8
|
refactor(module): 核心仓 Go module 路径改为 github.com/JianFeeeee/HologramAgent
批 1,不含 SDK 仓与目录改名。
- go.mod 第 1 行 module 路径
- 366 个 .go 文件的完整前缀 github.com/JianFeeeee/HomeAgent 机械替换
(857 处,仅完整前缀;展示名/注释里的 HomeAgent 字样留给批 5)
- 未改:third_party 目录名、SDK 自身 go.mod module、go.mod replace 目标、
homed/waiter 可执行名、.hmap 后缀
|
2026-10-21 12:17:19 +08:00 |
|
|
|
3119d48625
|
refactor(terminal): 终端逻辑回归 agentcli,内核不再编码终端语义
按用户指正调整架构:终端是 agentcli 的职责,内核不该有终端逻辑;插件与
agentcli 的联动应走事件总线,而不是内核内部组件。
## 删掉内核里的终端语义
`internal/agent/core/terminal_registry.go`(280 行)整体删除。它做了两件不该
内核做的事:定义「什么是终端会话」、从 EventToolCall 的 Go map 文本里解析
terminal id / command 并归并会话与命令历史。连带移除:
- Agent 的 terminalReg 字段、构造、subscribeTerminalRegistry 订阅
- StatusAPI 上的 ListTerminals / CmdHistory
- sdk.TerminalAPI + PluginSDK.Terminal() + Registry.SetTerminalAPI 及接线
- 对应测试(迁到插件侧视图包)
保留 `sdk.TerminalStatus` / `CmdExecStatus`:它们只是线上 JSON 格式,WebUI/CLI
返回给前端时口径一致。
## 插件侧订阅事件总线自攒视图
新增 `internal/pluginview`(TerminalView):把原先内核那份归并逻辑原样搬过来,
由插件自己持有。数据源仍是两条总线事件:
EventTerminalOutput(agentcli 发)→ 会话输出 + 生命周期
EventToolCall (内核发) → cmd_run 历史、terminal_create/close 归并
WebUI 在 NewHandler 订阅、Close 退订;CLI 在 Start 订阅、Stop 退订。
两边共用同一实现,避免历史上「两份推导各踩一次 Go map 文本」的坑。
视图不是权威:真值在 agentcli,插件晚于会话启动会缺早期会话(注释写明)。
## agentcli 保持内置
它现在仍需要 `Publish`(进程内事件总线写入)——外部插件没有上行事件通道
(`EventPublish` 在 withheldCapabilities 里,proc 只有 events.subscribe)。
按判定标准留内置;将来要外置,须先开一条受控的插件→内核上行事件通道,
本次刻意不开(用户决定:先保持内置)。
## 文档
中英 ARCHITECTURE 新增「终端归属」小节:说明职责边界、两条事件、视图非权威、
以及 agentcli 为何仍内置;内置判定表里 cli/webui 去掉 TerminalAPI 一项。
## 验收(本机实例实测)
- 内核编译通过,删除后 `go build ./... && go vet ./...` 干净,`go test ./internal/...` 全绿
- 实例重启后:让 agent 开终端跑 `echo term-view-test` →
WebUI `GET /api/v1/terminals` 返回 term_1(output 正确、running=false)
CLI `/terminals`(unix socket)返回同一份;
两处都是从事件总线自攒,内核侧无终端代码参与
- cmd 历史分支由迁过来的单测覆盖(Go map 文本解析、生命周期、上限裁剪)
|
2026-10-20 09:33:08 +08:00 |
|
|
|
b9097986c8
|
fix(prompt): 拼装时空行翻倍 + 人格支持版本占位符 + 配置项说明对齐分层原则
## 1. 提示词里的空行翻倍(用户实测发现)
抓包看实际 payload,system 里出现 3 处连续 3~4 个换行:
…别包装成"稍等我看一下"。\n\n\n【人格设定】
…/mascot.webp 直接能看。\n\n\n\n【场景记忆】
…能否同轮发出。\n\n\n【输出规则】
根因:拼装统一用 `prompt += "\n\n" + block`,而块自己常以换行结尾
(人格块是 "【人格设定】\n%s\n",自定义 system_prompt 也常带尾部换行)。
空行不致命但推高 token,也让「提示词到底长什么样」难以肉眼核对。
修法:新增 appendPromptBlock(trim 块两端 + 只拼一个 "\n\n"),所有拼装点改用它。
重新抓包:连续 3+ 换行 0 处。
## 2. 人格块支持版本占位符
buildSystemPrompt 只对 systemPrompt 调 expandPromptVars,人格块没展开 ——
人格里写死版本号仍会随发版说谎(实测线上人格卡写 v1.0.3)。现在人格同样展开
{{kernel_version}}/{{kernel_commit}}/{{sdk_version}}。
## 3. 人格/系统提示词两个配置项的说明重写
- core.agent.personal_prompt:DisplayName 改为「人格设定(称呼/语气/说话方式/形象)」,
说明只写人设;工具用法写工具描述、多步流程写技能;支持占位符;文件优先。
- core.agent.system_prompt:DisplayName 改为「系统提示词(身份与行为规则)」,
说明只写跨工具/跨通道成立的内容;运行时通道投递/并行/时序语义由内核注入无需重复;
留在提示词里的工具用法会随插件外置而失真。
## 4. 【记忆清理指令】→【记忆维护】
具体选哪个 memory_* 工具、各自副作用早已写在工具描述里(【记忆清理】前缀),
提示词里那份逐条列举属于重复。只保留「必须真做、不能只描述计划」这条
不看工具描述也会犯的错,并要求用一句人话说结果。
## 5. 新增 opt-in 提示词抓包(排查用)
HOMED_PROMPT_DUMP=<path> 时把每轮实际发给 LLM 的 messages 落 jsonl(0600)。
环境变量而非常规配置项:内容是用户隐私,只该在诊断时临时开。
本次排查即由它拿到证据;诊断后已移除临时 drop-in。
验证:go test(agent core / config / sdk / healthcheck)全绿;
实例重启后抓包复核空行归零;对话口吻仍为「短、口语、不列清单」。
|
2026-10-20 09:16:07 +08:00 |
|
|
|
48ca95f964
|
fix(onnx): 启动日志错报「走规则引擎」—— 实际一直用内嵌 ONNX 模型
用户质疑「编译的内核没启用 onnx」。核查结论:**ONNX 是启用的**,是我上一版
日志改错了,误导了判断。
## 事实
- 编译:`make build` 默认 `HOMED_TAGS=onnxruntime`(Makefile 有明确注释,
产物里 `go version -m` 可验 `-tags=onnxruntime`、CGO_ENABLED=1)。
- 多模态向量空间:`provider=chineseclip dim=512` 已加载,健康快照
`onnx.enabled=true`。
- 依存句法分析器:`core.agent.onnx_model_path` 留空时走**二进制内嵌模型**
(`//go:embed models/*` → 解到 `<data>/nlp/dep_parser.onnx` 后加载),
同样是 ONNX 推理,不是规则引擎。
## 上一版错在哪
我只回显了**配置值**(空串),于是打出
`dep parser ready (内置规则引擎;未配置 ...)` —— 把「内嵌模型已启用」
说成了「降级到规则引擎」。原始日志 `initialized (model: )` 的空括号
本来就是同一个信息缺失,我把它改成了**更确定的错误结论**。
## 修法
- `nlp.ONNXParser` 记录实际加载的 `modelPath` 并暴露 `ModelPath()`
(stub 构建下恒为空,两套构建接口一致)。
- 内核启动日志改为报实际路径:
`dep parser ready (ONNX,模型: /home/newqqagent/nlp/dep_parser.onnx)`
部署后实录:该行正确输出实际模型路径;`onnx.enabled=true`;
启动告警仅剩两条 CLI 型技能降级说明(已在上一个提交解释)。
|
2026-10-20 09:04:11 +08:00 |
|
|
|
16c9d4f69e
|
fix(startup): 启动阶段两类噪声归零 —— 插件 stdout 日志与 CLI 型技能误判
用户反馈启动日志里有报错。实测定位到两处,都是真实问题:
## 1. huawei_smarthome 往 stdout 打日志(8 条「收到非法 JSON 帧」)
stdout 是 JSON-RPC **协议通道**,它在 plugin.go/handlers.go/service.go 等处
用了 fmt.Printf(31 处),内核每收到一行就把协议解析失败一次。
这不是内核的错,是插件写法错 —— 已在其源码(/home/newqqagent/plugindev/
huawei_smarthome)把 fmt.Printf/Println 全部改成 log.Printf(stderr),
重新 hmapdev 打包并安装。重启后非法帧 0 条、10 个工具正常注册。
## 2. clawhubadapter 把 CLI 型 main.py 当 JSON-RPC sidecar
判定规则原为「有 main.py 就按 pysidecar 拉起」,但 ClawHub 上大量技能的
main.py 是 **CLI 入口**(`python3 main.py <tool> '<json>'`),对它发 ping
必然超时(bilibili、gaode 各等 5 秒)。旧代码只打一行错误就完事 ——
启动日志一片红,而技能既没变成工具、也没交给 skillmgr,等于半死不活。
改为:sidecar 就绪失败即降级为「CLI 型技能」,移交 skillmgr 并给出说明,
日志措辞从「错误」改为「未就绪 → 按 CLI 型处理」。两个技能现已可经
skill_info 读到文档、再用命令执行。
## 3. 顺带修一条看着像配置泄漏的日志
`dep parser initialized (model: )` —— 空括号是「未配置 ONNX 模型、走内置规则
引擎」的正常回退。改为显式说明,避免误读成缺配置。
## 验收(本机实例重启后实录)
- 「非法 JSON 帧」0 条(此前 8 条)
- dep parser 日志:`ready (内置规则引擎;未配置 core.agent.onnx_model_path)`
- 剩余 3 条均为说明性:mail-bridge 出站通道兜底、两个 CLI 型技能降级移交
- 13 个内置 + 13 个自启外部插件全部 loaded,agent main started
|
2026-10-20 09:00:58 +08:00 |
|
|
|
b5f024b054
|
refactor(prompt): 提示词去工具化 + WebUI 插件状态按运行期真相展示
## 1. 系统提示词/人格瘦身(工具用法搬去 tooldef 与 skill)
用户指出:默认系统提示词与 personal 里有大量本该属于 skill / 工具描述的内容。
核对属实,且已造成**真实故障**:提示词教模型用 cmd_run,而 cmd 外置按需后
工具根本不在清单里 ⇒ 模型第一轮调用不存在的工具,拿到「工具 cmd_run 不存在」。
改动:
- kernelmod/agent.defaultSystemPrompt:删掉 output_* 用法、工具分类清单、
cmd_run/terminal/files 策略、并行策略、媒体工具清单、emoji 风格要求。
只留:身份 + 「工具描述与技能索引会注入,先读再用」+「只依据工具返回值作答」。
- config.DefaultPersonaPrompt:只留身份/称呼/语气 + 版本以运行时快照为准
(后者由 TestDefaultPersonaPromptHasNoVersionLiterals 钉住)。
- 命令与文件策略下沉到工具描述:cmd_run 补「完整 bash,支持管道/heredoc/重定向」,
files_write 补「原子替换 + 覆盖受保护路径前留档」。
- 对话时序块自带语义说明(原先靠提示词解释,提示词一精简就丢语义)。
- 文档新增「提示词分层」章节(中英),写明归属判据与这次的反面教材。
## 2. WebUI:插件「启动状态」按运行期真相展示
旧实现只看内核 status 的 loaded 一个布尔,三种形态被混成同一个「未加载」:
auto_start=false 的按需待命(正常)、该起没起(异常)、plugin_stop 后的运行态停止。
用户看到一屏「未加载」以为插件全坏了。
- sdk.PluginRuntimeInfo 新增 AutoStart / RuntimeStopped;registry 从清单填充。
- pluginmgr /plugins 列表暴露 auto_start / runtime_stopped / builtin / tool_count。
- webui 新增 POST /plugins/<name>/start|stop(运行态,不动持久禁用清单)。
- dashboard 状态列改为五态:已禁用 / 进程丢失 / 已运行 / 按需待命 / 未运行,
并额外标注「自启 / 按需」;按钮改为 启动/停止(运行态)与 禁用/启用(持久态)分列。
- 新增两条回归判据(引用 auto_start/runtime_stopped/alive 字段;startPlugin 走 /start)。
## 验收(本机实例实测)
- files 保持 auto_start=false(按需);问 agent 写文件 → 它先 plugin_start cmd+files,
再 files_write/files_read,内容校验一致;未用 shell 代替文件操作。
- WebUI 插件页截图确认:a2a/acp/agentcli 显示「已运行 自启」,
ai_image 显示「按需待命 按需」+「启动」按钮。
- go test(config/agent core/plugin/pluginmgr/webui/sdk)全绿。
|
2026-10-20 08:57:36 +08:00 |
|
|
|
9b2c74bc49
|
refactor(config): 默认值单一来源 —— ConfigDef.Default,不再各调用点重复硬编码
## 起因(用户质疑)
我把 offload 的触发阈值从 5 分钟改成 1 分钟时,同一个默认值散落在四处:
defaultOffloadBusyAfter = 5 * time.Minute (agent/core 常量)
set("core.agent.offload_busy_after", "5m") (seedDBValues 播种)
reg(ConfigDef{... Default: "5m" ...}) (seedCoreDefs 定义)
GetDuration("...", 5*time.Minute) (kernelmod 读取点)
用户问:「为什么不是单一从配置读取,而是硬编码在源码中?」
答:这不是合理的分层,是重复。同一份默认值四处字面量,改一处漏两处
就是**静默的行为漂移** —— 不报错,只是某些路径用 5m、另一些用 1m。
## 改动
**① 默认值的唯一来源 = ConfigDef.Default**
GetInt/GetDuration/GetBool/GetString 新增 defDefault():
DB 里查不到行时,从已注册的 ConfigDef.Default 取,而不是用调用方传的字面量。
调用点现在写 GetDuration(key, 0) —— 那个 0 只是「连 ConfigDef 都没注册」
时的兜底,正常路径永远用不到。
**② 删掉 seedDBValues 里 54 行重复的 set(),改由 reg() 落库**
reg() 原本只 `r.defs[k]=&d`(只登记不落库),默认值另由 set() 写一遍。
现在 reg() = registerDefLocked(登记 + 落库)。
为此抽出 registerDefLocked:SeedDefaults 已持 r.mu,直接调 RegisterDef
会自死锁(sync.Mutex 不可重入)。
**③ 8 个键必须「只登记、不播种」,新增 regMeta()**
core.agent.system_prompt 播种会把版本号冻死在 DB 里
core.defaults.llm_endpoints 空默认值
core.memory.multimodal_space.options.* provider 私有选项
第一版没有区分,一改就红了两条既有判据:
TestSeedDefaultsDoesNotSeedSystemPrompt
TestSeedDefaultsDoesNotInjectIntoLegacyInstall
后者尤其重要:老安装升级被注入新默认值 = 静默加载 1.8GB 向量模型。
**④ seeded 集合:回退只对「本次真播种过」的键生效**
这是修 ③ 过程中暴露的更根本的问题。GetXxx 无条件回退到
ConfigDef.Default 会让**老安装**读到新版本的默认值 —— 即使 DB 里
根本没有那一行。加了 seeded map 区分:
全新安装 → 键已播种 → 回退 ConfigDef.Default(正常)
老安装 → 新键没播种 → 返回零值(不注入)
## 顺带修的
offload_busy_after 的 Type 从 "string" 改成 "duration" —— 它本来就是时长,
用 string 时 WebUI 只能给文本框,拿不到时长控件。
## 判据
沿用既有判据,不新增。新写的 offload_threshold_test.go 已删除 ——
它断言「四处默认值必须一致」,方向是**鼓励重复**,与本次目标相反。
|
2026-10-18 10:16:53 +08:00 |
|
|
|
a68d6a428c
|
feat: 模块化内核(编译期依赖图定序)+ 密钥防火墙 + 插件三态生命周期 (#10)
* feat(kernel): 模块化内核 + 编译期依赖图定序
把单体 init 序列拆成「模块自声明依赖 + 编译期生成依赖图 + 运行期只读图」。
## 为什么
原 cmd/homed/main.go 里 17 步 init 是**手写顺序**,依赖关系只存在于
注释和人脑里。实测踩到的坑(见 docs/zh/kernel-modularization-design.md):
· nlp.defaultParser 被 initONNXParser 写、被 distiller 读,两者相距 60 行,
只靠 ticker 10min 才没炸(隐式依赖,无声明)
· 加一个模块要人肉找"该插在哪",插错的表现是运行期 nil 解引用
· 关停是 defer ks.CloseAll(),而热更新路径 os.Exit(42) **跳过 defer**
⇒ docStore/distiller/textMem 的 flush 全部丢失(每次热更新都发生)
## 怎么改
- internal/kernelboot/:KernelState(一个结构体收全部包级共享变量,字段
nil 安全)、Module 接口、graphfile(依赖图编解码 + 与二进制 hash 对账)、
scheduler(按 BFS 层并行 Init)、syncbus(层边界写通知)、
lifecycle(Quiesce/Resume 可逆伪关机,区别于一次性 CloseAll)
- internal/kernelmod/<name>/:每个模块一个文件夹,Requires/Provides 自声明;
all/all.go 是唯一的 import 清单(Go 循环导入约束)
- cmd/homed-depgraph:编译期生成依赖图,Makefile 注入 digest
- 运行期只**读**图,不计算顺序(顺序是编译期决定的,源码里没有中央清单)
## 判据
kernelboot 8 条 + 图失败模式 7 条(全部拒绝并给准确错误)+ 模块目录数
与注册表一致性。变异测试 15/15 咬住,含:
· 删掉 AddCloser 的锁 → -race 报数据竞争
· Scheduler 同批跑去重 → 203 vs 200
· 图 hash 不匹配 → 拒绝启动
## 顺带修的缺陷
热更新路径漏调模块关闭 ⇒ 显式 stopKernelModules(),并用 AST 判据钉住
"必须在 os.Exit 之前"(字符串检查在本仓已骗过我三次)。
实测:冷启动 1630ms(5 层/17 模块,原 17 步串行);生产 22 插件全部建链。
* feat(security): 密钥防火墙 + 配置加密 + close 纪律
## 密钥防火墙
模型只见别名、不见明文;插件工具调用入口(StageHost.ExecuteTool)做入向
回填。三维能力:
· 出向:工具输出里的密钥被替换成 [[secret:name]] 别名后才进上下文
· 入向:模型传别名,入口处填回真值(插件完全无感知)
· 存储:config.db 里不存明文(0644 的库可读),敏感值独立 0600 文件
模式库「宁漏勿误伤」:宁可漏掉一次替换,也不把正常文本误当密钥。
## 配置加密
- internal/security/{codec,key,keyring,store,global}.go:AES-256-GCM,
enc:v1: 前缀,密钥轮换(OldKeys 解码 + 新钥加密)
- harden.go:数据目录权限收紧
- embedded.go:支持内嵌主密钥(可选,仍保留外部 key 文件路径)
- EncryptExistingSecrets / AuditSecrets:启动时幂等迁移 + 审计
## close 纪律(用户明确要求:只要调 close 就要有前置检查)
internal/safeclose:Safe / SafeFunc。全仓 37 处裸 close(chan) → 0。
关键教训(都写进注释和判据):
· `select { case <-ch: default: close(ch) }` **不是**并发安全的
(两个 goroutine 可同时命中 default)—— healthcheck 原先是这种写法
· `sync.Once` **不可重入**:X.Do(func(){ Safe(&X, ...) }) 会自锁,
症状是 go test 直接挂住(3 个包 600s 超时),栈里只有嵌套的 Once.Do
· map 里按请求持有的 channel(mcp/stdio.go、clawhubadapter/sidecar.go)
改成 *pendingRPC 包装,每通道一个 once
## 判据
security 全量 + TestNoBareCloseOutsideSafeclose(AST 级扫描,含注释剥离)
+ TestNoNestedSelfOnceDeadlock(正则修正:\w+ 不匹配点号,捕获组要含 '.')。
变异 2/2 咬住。全量 52 包 ok。
* feat(plugin+deploy): 插件三态生命周期、数据目录迁移、发行包修复
## 插件三态生命周期(用户要求:自动启动 / 启用但不自动启动 / 禁用)
- Manifest 新增 requires_facilities / auto_start(*bool,用于区分
"未声明"与"显式 false")
- plugin-host 模块(internal/kernelmod/pluginhost):用 kernelboot.Scheduler
按依赖时机拉起,**不再手写两阶段**
- 运行态停启 StopPlugin/StartPlugin **可逆**:不写 disabled_plugins
(不改用户决定)、不置 shuttingDown(之后还能加载)、会摘掉注册面
- 与 StopAll 的区别写进注释 —— StopAll 置 shuttingDown 后**永久无法加载**,
那是进程退出用的,误用于"暂停"就是关了就死
## 数据目录迁移(伪关机 → 迁移 → 唤醒)
- kernelboot/lifecycle.go:Quiesce/Resume(可逆,区别于一次性 CloseAll),
逆序暂停、逆序恢复、Pause 失败回滚
- kernelboot/migrate.go:BeginMigration/End 编排(先停插件 → 再停内核写者
→ 等在途归零)
- distiller/textmem 增 Pause/Resume 门(不是 cancel —— cancel 不可逆,
恢复要重建整个对象)
- 模块 migration:体检 + 生成可执行迁移脚本
★ 迁移脚本的三个实测教训(都写成了判据):
1. 无条件改 /etc/systemd/system ⇒ 演练把**生产** -data 改成了演练目录。
加 --dry-run(零改动)+ 强制校验 systemd 当前 -data == OLD
2. 本机没装 rsync,脚本在"服务已停、数据未搬"处硬失败 ⇒ cp -a 回退,
并明确警告不保硬链接/ACL(生产 plugindev 有大量 git 硬链接)
## 发行包修复(全部经实测确认)
1. package-linux.sh **完全漏打 depgraph.bin** ⇒ 内核 exit 1
("生产构建(内嵌 hash 非空)但读不到依赖图")
2. build_homed 不走 Makefile ⇒ 单独修 Makefile 无效,必须同时生成图+注入
3. build.sh 的 LDFLAGS 用**历史模块路径 gitcode.com** ⇒ -X 静默 no-op
⇒ --version 显示 unknown
4. Makefile -ldflags 用**单引号** ⇒ 注入了字面 $GRAPH_HASH
## ★ build.sh 的版本号必须校验(真实事故)
原 `VERSION="$(git describe --tags --dirty)"`。无 tag 分支上它返回**分支名**
`backup/pre-identity-rewrite-20261005-51-g03d87a4e-dirty`,被注入 meta.Version
⇒ 插件握手判据 MajorVersion() 把整串当主版本 ⇒ **22 个子进程插件全部
握手失败**("主版本不匹配")。内核活着、systemd active、日志每条都像
插件的问题 —— 实际是发行包带着非法版本号。
修法:git describe 只在形如 vX.Y.Z 时采用,否则回退 meta.Version(权威源),
并对最终值做形态守卫。deploy/packaging/version_guard_test.go 3 条判据,
变异 2/2 咬住。
## deb 的 systemd unit 改为 guard 模式
ExecStart=/usr/bin/homed --role=guard --data /var/lib/homeagent
(worker exit 42 → guard 重拉,不经过 systemd、不计失败)
含 RestartSteps/RestartMaxDelaySec/TimeoutStopSec + 硬化
(NoNewPrivileges/ProtectSystem/ProtectHome/DevicePolicy/ReadWritePaths)。
## 其他
- internal/plugin/proc:非法 JSON 帧的报错**可定位**(内容摘要 + 可能原因 +
修法)。生产实测 1038 条非法帧(插件往 stdout 打日志 = 污染协议通道)
- 示例插件 4 个 14 处 fmt.Printf → log.Printf
- hmapdev 模板同步修复;scaffold 判据剥离注释(模板注释里就含 fmt.Printf)
* chore: 收尾(主密钥轮换命令、IPC/知识库加固、SDK 对齐)
- cmd/homed-key-rotate:把 security 里的 RotateKey/RotateKeyFile/RekeySecrets
串成一条**可执行**的运维路径。没有它,E-1 就只是库里的能力而不是
"出事时能做的事",而轮换的价值恰恰在于能执行。
- cmd/homed:kmod_runtime(模块装配入口)、plugins_scheduled(failback
白名单)、memcandidate、bootstrap_order_test / shutdown_flush_test
- internal/ipc、internal/devicebridge、internal/knowledge:配套加固
- third_party/homeagent-sdk/sdk:PluginMgrAPI 增三态能力(mgrStop/mgrStart/
mgrIsRuntimeStopped)+ settings 对齐
判据:bootstrap_order_test(启动顺序不依赖手写序列)、
shutdown_flush_test(停机的两条路径必须对称 —— exit 42 会跳过 defer,
热更新路径下模块关闭/flush 曾全部丢失)。
* test(security): 测试夹具改用不可误认的占位值(修 push protection 拦截)
GitHub secret scanning 拦住了推送:三处测试夹具用了"看起来像真密钥"的
字面量:
internal/security/prefilter_test.go:39
internal/security/store_test.go:177
internal/security/survey_test.go:20
它们全是**假密钥**(`AKIAIOSFODNN7EXAMPLE` 是 AWS 官方文档的标准示例,
其余是 `abcdefghij...` 这类可读占位符),但扫描器无法区分 —— 它只看形态。
★ 不绕过(不用 unblock URL、不加 allowlist):直接改夹具。
原因:一旦允许"假密钥可以通过",真密钥混进来时就没有最后一道门了。
改法:保留**模式库匹配所需的结构**,把可识别内容换成 FAKE:
glpat-abcdefghijklmnopqrst → glpat-FAKE-TEST-TOKEN-NOT-REAL
sk-abcdefghij...012345 → sk-FAKE-TEST-KEY-NOT-REAL-0000000
AKIAIOSFODNN7EXAMPLE → AKIA0000FAKE0000TEST (保持 20 字符)
ASIAIOSFODNN7EXAMPLE → ASIA0000FAKE0000TEST
xoxb-123456789012-abcdefghijklmnop → xoxb-FAKE-TEST-TOKEN-NOT-REAL
★ 中途踩到一次:AWS 键的模式是 `\b(AKIA|ASIA)[0-9A-Z]{16}\b`(共 20 字符),
我第一次替换成 19 字符的 `AKIAFAKETESTKEY0000` ⇒ 模式匹配不上 ⇒
TestSurvey_WhatIsCaught / TestRecognizesCommonShapes /
TestPrefilterNeverBlocksRealMatches 三条当场变红。
判据正确地拒绝了"为了过扫描器而削弱覆盖"。
顺带核实:全仓 grep 无任何真实密钥泄漏。
* fix(packaging): package-linux.sh 的版本解析与 build.sh 统一
上一版只修了 build.sh,漏了 package-linux.sh —— 它仍只看 git describe:
VERSION="${VERSION:-$(git describe --tags --dirty)}"
无 tag 分支上 git describe 返回**分支名**
(backup/pre-identity-rewrite-20261005-56-g98850e1),守卫会正确拒绝,
但要求人**手动传 VERSION** 才能打包。而 build.sh 早已能自动回退 meta.Version
⇒ 同一个版本,走 build.sh 打得出来、走 package-linux.sh 打不出来。
危害不只是"要手动传参":运维为了跑通会去掉守卫,而分支名一旦进
meta.Version 就是**插件协议主版本**的来源 ⇒ 22 个子进程插件全部握手失败
(这是真实发生过的事故,见 3efa4e5)。
修法:两个脚本用同一套规则(git describe 仅在 vX.Y.Z 形态时采用,
否则回退 meta.Version),并对最终值做形态守卫。
判据:TestBothPackagingScriptsShareVersionRules —— 逐个读两个脚本,
要求都能从 meta/meta.go 取权威版本、都校验 git describe 形态。
变异(把 package-linux.sh 改回旧规则)咬住并准确指出是哪个脚本。
实测:不传 VERSION 跑 package-linux.sh,输出
[PKG] warn: git describe 给出非版本串(...),改用 meta.Version=1.4.0
Version: 1.4.0
* fix(test): 跨仓判据在 CI 上应 skip 而非 fail(修 PR 双红)
CI 上 "Go build / vet / test" 与 "Build linux/amd64" 双红:
--- FAIL: TestHmapdevTemplateImplementsAllPluginMgrMethods
bridge_sync_test.go:170: 读模板: open
../../../third_party/homeagent-sdk/tools/hmapdev/templates/proc_main.go.tmpl:
no such file or directory
## 根因:判据依赖了一个**被 gitignore 的路径**
`.gitignore:33` 有 `third_party/homeagent-sdk/tools/`(SDK 仓自治,本仓不跟踪)。
于是:
· 本机(完整 SDK 检出)有该文件 ⇒ 判据通过
· CI(只 checkout 本仓)没有该文件 ⇒ Fatalf ⇒ **必红**
我提交时只在本机跑测试、没看 CI,所以这个问题拖到 PR 才暴露。
## 修法
文件缺失时 `t.Skipf` 而不是 `t.Fatalf`。
★ 这不是"放弃检查":
· 跨仓一致性由 **SDK 仓自己的判据**守着
(tools/hmapdev/scaffold_stdout_test.go 等),那里才是模板的真实归属地
· 本判据在本机开发时**仍然生效**(已验证:有文件时 PASS,缺文件时 SKIP)
## 验证
· 本机(有文件) → PASS(判据真跑)
· 模拟 CI(移走 tools/)→ SKIP(不再 fail)
· 全量 53 包 ok / 0 fail
## 顺带
同目录还有 e2e_template_test.go 也读 tools/,但它是 `//go:build` 门控的
e2e 判据,CI 默认不跑,无需处理。
---------
Co-authored-by: JianFeeeee <JianFeeeee@users.noreply.gitcode.com>
|
2026-10-17 19:18:24 +08:00 |
|