mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 14:57:38 +00:00
## 为什么必须是第 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 一致)。
57 lines
2.6 KiB
Go
57 lines
2.6 KiB
Go
package kernelmod
|
||
|
||
// 设施名常量(依赖图上被引用的名字)。
|
||
//
|
||
// ★ 为何用常量而不是裸字符串:拼错依赖名时**编译不过**,
|
||
// 而不是"永远等不到就绪"(运行期表现是静默挂起 / 静默拿到 nil,极难排查)。
|
||
//
|
||
// ★ 为何放在 core.go 之外:用户要求"根目录只有 core.go",
|
||
// 而 core.go 专注注册表职责。设施名是**模块间的协议**,与注册表分开更清晰。
|
||
const (
|
||
// ── 插件可依赖的设施(与公开 SDK 的 sdk.Facility* **逐字一致**)──
|
||
//
|
||
// ★ 判据 TestFacilityNamesMatchPublicSDK 守这个一致性。
|
||
// 这些名字会出现在 plugin.json 的 requires_facilities 里。
|
||
FacConfig = "config"
|
||
FacKernelIO = "kernel-io"
|
||
FacMemory = "memory"
|
||
FacLua = "lua"
|
||
FacSupervisor = "supervisor"
|
||
FacTracker = "tracker"
|
||
FacTextMem = "textmem"
|
||
FacLLM = "llm"
|
||
FacDocStore = "docmem"
|
||
FacKnowledge = "knowledge"
|
||
FacMultimodal = "multimodal"
|
||
FacONNX = "onnx"
|
||
FacStageHost = "stagehost"
|
||
FacAgent = "agent"
|
||
|
||
// ── 内核**内部**顺序标记(**不是**给插件的设施)──
|
||
//
|
||
// ★ 为什么不与上面的混在一起:这两类东西的**消费者不同**。
|
||
// · 上面的:插件按它声明启动时机(对外契约,必须进公开 SDK)
|
||
// · 这里的:只用于**内核模块之间**表达顺序(如"迁移必须先于扫盘")
|
||
//
|
||
// 混在一起会让"内核内部标记"被误当成"插件可选设施"
|
||
// ⇒ 插件可能声明它(然后永远等不到 —— 因为它的就绪时机是装配中途,
|
||
// 而插件提交发生在装配**完成之后**……实际会立刻满足,但语义是错的:
|
||
// 它不该成为插件的契约)。
|
||
//
|
||
// 判据 TestFacilityNamesMatchPublicSDK 只比对上面那组 ⇒ 天然拦住
|
||
// "把内部标记泄漏成对外契约"。
|
||
|
||
// FacMigrated 标记"知识库目录迁移已完成"(用于让 store 排在迁移之后)。
|
||
FacMigrated = "knowledge-migrated"
|
||
|
||
// FacVersion 标记"内核版本自检与升级回滚判定已完成"(update 模块提供)。
|
||
//
|
||
// 语义:读到它时,ks.Update 里的版本已经是**回滚后的最终版本**。
|
||
// 需要按版本行事的模块(healthcheck 上报版本、pluginhost 判插件兼容性)
|
||
// 应 Requires 它,否则可能读到升级前那个已被回滚的版本。
|
||
//
|
||
// 为什么在内部标记段:它是内核自用的启动时序事实,不是插件可选的
|
||
// 外部契约(插件声明它只会立刻满足,没有意义)。
|
||
FacVersion = "kernel.version"
|
||
)
|