|
|
129a3aeb38
|
refactor(proc): coreHandler.Handle 550 行按 method 组拆成 14 个分部函数
原 Handle 是一个 550 行的巨型 switch(C ABI 51 个 case 的整块平移),
按协议面拆进同包 5 个新文件、14 个小函数:
corehandler_register.go handleRegister 注册面(tool/stage/output/api/input)
corehandler_inject.go handleInject IO 注入 + SetToolBlocks
corehandler_memory.go handleGraphMemory 图记忆
handleDocMemory 文档记忆
handleKnowledge 知识库
handleTextMemory 文本记忆
corehandler_settings.go handleSettings 设置(14 个 method 共用一条实现)
handleLLM LLM 源
handleSocial 社交图只读
handleLifecycle 生命周期开关
corehandler_runtime.go handlePluginMgr 插件管理
handleStageLocks 段锁仲裁
handleEvents 事件订阅
handleArena 共享槽池
Handle 保留 capability 强制检查,只做「method → 分部函数」一跳。
零漂移保证:case 标签由脚本从原文提取(不手抄常量名),case 体逐字搬迁,
逐函数比对确认 59 个标签 / 510 行 case 体与原文件完全一致(仅行首缩进经 gofmt 重排)。
验证:go build ./... / go vet / go test ./internal/plugin/... / -race 全绿。
|
2026-09-13 23:29:13 +08:00 |
|
|
|
dcae21b24c
|
refactor(lua): replaceSDKReal 633 行按 SDK 子表拆分
抽 luaReg 上下文(L/t/plg/s + subTable/pushVal/pushList/pushErr/pushNil
五个助手方法),把单函数拆成 registerRegistrars / registerInjectors /
registerDataAPIs / registerSettings / registerEventsAndMgr 五个方法。
被移动的代码体**逐字保留**:各方法头部把 L/t/s/plg 与五个助手注入为局部
别名,所以内部一行未改,行为零漂移。luaSyncUnavailable 提为包级函数
(原来它在被拆到另一个方法的作用域里会 undefined)。
实测:replaceSDKReal 从 633 行消失,最大子方法 265 行;非测试函数
>=300 行的数量 3→2。go build/vet/test -race 全绿。
|
2026-09-13 22:44:39 +08:00 |
|
|
|
62e1e02036
|
docs(ohos): README 两处事实修正(产物路径前置条件、≤400 行约定的边界)
1. **安装小节的产物路径**:`entry/build/` 是纯构建产物、不入库,干净 clone 或清理过
工作区时该文件不存在。补明"必须先跑完第 2 步",并说明目录名随
`-p product=<名字>` 变化、同目录还有 unsigned 版(`hdc install` 要用 signed)。
本次实际构建校验过路径:`entry/build/default/outputs/default/entry-default-signed.hap`。
2. **≤400 行约定补边界**:不到 400 行的文件不要为了拆分而拆分 ——
`@Component` 的 `build()` 只允许一个根节点,多节点 `@Builder` 改组件会多出一层
Column 包裹,布局等价是推理出来的、不是看出来的,每拆一次都要付一次
"未上机验证"的账。并记下 `pages/Index.ets`(396) 属于"不越线就不动"的一类。
无代码改动,纯文档。
|
2026-09-13 22:42:37 +08:00 |
|
|
|
8a58cde3ee
|
docs(ohos): 本地构建前置条件 + 运行时验证清单 + 工程结构表刷新
1. **oh_modules 前置条件**:该目录被 .gitignore 忽略但必须存在 —— hvigor 不会
自动 ohpm install,移走后构建直接报 arkts-no-untyped-obj-literals(依赖类型
声明缺失)且不重建目录。写明"clone 后先 ohpm install,别当垃圾清掉",
同时说明 entry/build 与 .hvigor 是纯产物、可随时删(冷构建 ~8s)。
2. **hvigorw 绝对路径**:仓库里的 ./hvigorw 是符号链接,启动脚本按
$(dirname $0) 定位,会报 File not found: <repo>/cmd/ohos/hvigor/bin/hvigorw;
统一改用 <command-line-tools>/bin/hvigorw(本机 /opt/huawei/command-line-tools/bin/hvigorw)。
3. **运行时验证清单**:把"构建通过 ≠ UI 行为不变"这件事写进仓库而不是留在邮件里
(流式对话/思考卡与工具卡/附件上传/设置与设备与插件的二级页/宽屏分栏),
并注明无提权环境起不来模拟器时需在真机补验证。
4. 工程结构表按前几轮拆分后的实际情况刷新(common/components/pages 新增文件与职责),
并记下"单文件 ≤400 行、页面只做壳"的约定。
无代码改动,纯文档。
|
2026-09-13 22:35:29 +08:00 |
|
|
|
e733d05a5e
|
refactor(ohos): DevicePage 560→307 行(入口列表/四个面板/模型拆分)
- common/DeviceModel.ets(73):device_id 兜底(桥 id 优先 → 持久化 → 生成并落盘,
顺序与原 aboutToAppear 一致)、/device/online 响应解析、四个二级页路由 id
- components/DeviceRootEntries.ets(92):一级入口(本机/通道两组 NavRow 列表);
宽屏高亮自己读 AppStorage 的 isWideScreen,页面只给 activeSub 与 onOpen
- components/DevicePanes.ets(316):DeviceLocalPane(基本信息 + 授权开关)、
DeviceCapsPane(能力清单)、DeviceGatewayPane(网关信息 + 刷新)、
DeviceListPane(在线设备列表)、DeviceKvRow;每个面板自带 SubPageLayer
页面保留导航栈、openSub/closeSub、授权开关、toast、refreshDevices 与
SubDestination 分发。所有文案、图标、颜色、过渡与失败分支逐字保留;
面板内容多了一层无 padding 的 Column 根节点(@Component 的 build() 只允许
一个根),宽度 100% + Start 对齐,与 SubPageLayer 内容槽的布局一致。
验证:hvigorw assembleHap BUILD SUCCESSFUL。
|
2026-09-13 22:34:20 +08:00 |
|
|
|
8a9fc05451
|
refactor(ohos): DeviceBridge 409→296 行(协议类型与帧构造移到 BridgeProtocol)
- common/BridgeProtocol.ets(168):hello/bind/cmd_result/cmd_data_start/
cmd_data_end/event/status 七个消息结构、CHUNK_SIZE、BridgeCmdHandler 回调类型,
以及 bridgeHelloFrame / bridgeBindFrame / bridgeResultFrame /
bridgeDataStartFrame / bridgeDataEndFrame / bridgeEventFrame /
bridgeStatusFrame / bridgeChunkSlices 八个纯构造/切片函数
- common/DeviceBridge.ets(296):只剩 socket 生命周期、重连代际、绑定状态机
与命令分发
**DeviceBridgeClient 的对外方法名与签名一字未改**(isConnected / getDeviceId /
setCmdHandler / setStateListener / connect / updateAuthorized / disconnect /
sendResult / sendDataChunked / sendEvent / sendStatus / send),
`send*` 仍是"拼帧 + 发出去"两步,拼帧那一步搬走;分块发送的
"start → chunks(失败即 break)→ end"顺序与 `bytes.slice` 语义保持不变。
外部只有 deviceBridge 单例被 import(BridgeRouter / DeviceBridgeSession /
DevicePage),无其它符号依赖。
验证:hvigorw assembleHap BUILD SUCCESSFUL;diff 中 DeviceBridge.ets 无
状态机/回调/重连逻辑改动。
|
2026-09-13 22:28:53 +08:00 |
|
|
|
31bbde9cdd
|
refactor(ohos): Attachment 463→344 行(纯函数与字节解码移出 components)
- common/AttachmentMeta.ets(106):parseAttachment / attachmentFromChannelOutput /
fileNameOf / formatBytes / oneDecimal / extLabel / sanitize,纯函数无平台依赖
- common/AttachmentImage.ets(37):loadPixelMap(沙箱 file:// 与远端 /files 两条路径)
components/Attachment.ets 只留 UI:AttachmentCard 与 AttachmentDetailContent。
**两者的导出名、@Prop 形状与 import 路径均未变**(ChatPage 仍从
'../components/Attachment' 取 AttachmentDetailContent),组件内部的布局、
过渡、提示文案与失败分支逐字保留,仅函数体搬走。引用方 5 处 import 路径同步更新。
验证:hvigorw assembleHap BUILD SUCCESSFUL;diff 中 Attachment.ets 只有
"-删除纯函数 + import 改写",无属性/布局改动。
|
2026-09-13 22:27:34 +08:00 |
|
|
|
e0514c3692
|
refactor(ohos): ChatPage 1962→207 行(状态机/SSE/气泡/输入区拆分)
- common/ChatStore.ets(388):消息数组、分页游标、新消息入场标记、
SSE 连接与重连、防抖刷新(50ms)统一成一个单例状态机
- common/ChatSse.ets(192):SSE 事件 → 状态的翻译层,逐分支照搬
channel_output/agent_output/reasoning/delta/tool_call/stage/agent_error/
sync_required;通过 ChatStreamSink 接口写入,避免与 ChatStore 形成循环依赖
- common/ChatSession.ets(176):POST /chat 与 POST /chat/file 的发送、
超时兜底、POST 响应与 SSE 的合并判定
- common/ChatFormat.ets(223):mime 推断、ForEach 键 structSig、工具卡
状态/颜色、渠道判定与头像配色
- common/ChatHistory.ets(96):历史载荷与 tool_calls 解析
- components/ChatBubble.ets(247):气泡(头像/渠道名/思考卡/工具卡/附件卡/正文)
- components/ChatToolCard.ets(253):思考卡 + 工具卡
- components/ChatStream.ets(210):消息列表 + 顶栏遮罩 + 底部淡出 + 触顶懒加载
- components/ChatComposer.ets(357):输入行、选图/选文件、沙箱落盘、发送
- components/ChatAttachBar.ets(139):加号菜单 + 待发送附件条
响应式语义刻意保持不变:数组不进 AppStorage,改用自增版本号 K_CHAT_REV
通知订阅组件重取快照(ChatStream 把它镜像进 @State messages,ForEach 每次
拿到的仍是新数组引用,与拆分前 this.messages = this.messages.slice() 等价);
structSig 仍不含 content,正文靠 MarkdownView 的 @Prop 流式更新;
markNew 仍不切片、入场动画交给紧随其后的 refresh();
animateTo 只能存在于组件里,所以折叠翻转的动作留在 ChatStream 内。
验证:hvigorw assembleHap BUILD SUCCESSFUL;SSE 各分支、ensureToolCall、
markNew、sendChat/sendWithAttachment 的守卫顺序均与原实现逐条比对过,
守卫从"页面方法内"移到输入区组件时保持了原有先后(连接判定先于清空输入)。
|
2026-09-13 22:19:05 +08:00 |
|
|
|
f79e0f82dd
|
refactor(ohos): SettingsPage 1463→389 行(模型/条目卡/四个面板拆分)
- common/SettingsModel.ets(313):设置载荷解析、分类归并、分页切片、
路由常量,纯逻辑无 UI
- components/SettingsEntryCard.ets(215):单条设置卡(值编辑/保存)
- components/SettingsRootEntries.ets(100):一级入口行 + 状态汇总卡
- components/SettingsHome.ets(95):一级页壳(顶栏/浮层/提示条)
- components/ConnectionsPane.ets(347):后端连接 CRUD(自带二级页壳)
- components/AppearancePane.ets(265):主题/背景/透明度(自带二级页壳)
- components/BackendSettingsPane.ets(172):分类与条目两个二级面板
页面保留 @State 集合、加载/保存编排与 SubDestination 分发;数组仍走
@Link 直传(未引入 AppStorage 数组)。相册选择、重启前台桥等既有行为
与提示文案逐字保留。
验证:hvigorw assembleHap BUILD SUCCESSFUL。
|
2026-09-13 22:18:56 +08:00 |
|
|
|
131c1ff0e9
|
refactor(ohos): PluginsPage 995→374 行(接口/状态/列表/详情/浮层拆分)
拆分方向按职责切,页面只留导航与数据编排:
- common/PluginApi.ets(174):fetchPluginRows / fetchPluginDetail
- common/PluginStatus.ets(78):状态判定、文案、颜色、副标题
- components/PluginListView.ets(166):列表卡 + 列表
- components/PluginDetailPane.ets(291):详情面板
- components/PluginsOverlays.ets(65):安装表单
- components/ToastBar.ets(48):提示条,PluginsToast 与 SettingsPage 的
toast 合并成这一个组件(此前两处各写一份)
UI 结构与文案逐字保留(含 92%/layoutWeight 等既有布局修法)。
验证:hvigorw assembleHap BUILD SUCCESSFUL。
|
2026-09-13 22:18:49 +08:00 |
|
|
|
414e6a1627
|
refactor(ohos): StaticMarkdown 604→332 行(Markdown 解析抽到 common/MarkdownParser)
把"文本 → 块/片段"的纯解析逻辑(MdBlock/MdSpan、parseBlocks、parseInline、
表格/分隔线/有序列表判定)整体搬到 common/MarkdownParser.ets(282 行),
StaticMarkdown.ets 只留 StaticMarkdownView 组件(332 行)。
解析规则逐字保留,未改任何排版行为;MdBlock/MdSpan 原本只被
StaticMarkdown.ets 使用(唯一引用方是 components/MarkdownView.ets,
它只用 StaticMarkdownView)。
验证:hvigorw assembleHap BUILD SUCCESSFUL。
|
2026-09-13 22:18:43 +08:00 |
|
|
|
431bb5a0c9
|
refactor(tooldefs): buildToolDefs 614→273 行(抽 toolDef 助手,schema 形状不变)
原实现每条工具都是 4 层嵌套的 map[string]interface{} 字面量(约 20 行/条),
40+ 条堆成 614 行的巨型函数。新增 toolDef(name, desc, props, required...)
助手消除外层样板;所有 name/description/properties/required 文本**逐字保留**
(用 go/ast 定位原样搬迁,非重新键入),schema 形状与 JSON 输出不变。
验证:go build ./... / go vet / go test ./internal/agent/core/ 全绿;
AST 实测 614→273 行,非测试函数 ≥300 行数由 4 降到 3。
|
2026-09-13 22:17:45 +08:00 |
|
|
|
6878f0126d
|
fix(lua): 同步注入在 Lua 中明确报不可用(避免自锁)+ 文档/mock 同步
sdk.inject_input_sync / *_opts / inject_input_media_sync* 在 Lua 里必然自锁:
Lua 代码只在 Start/工具/阶段/输出/事件回调中执行,这些路径都持有 plg.mu,
而同步注入要等本轮回复(回复路径上的回调又需要同一把锁)。原实现会挂死
直到超时;现改为立即返回明确错误,并在中英文 PLUGIN_DEV 里标注不可用 +
指向 Go 插件/异步注入。mock sdk.lua(SDK 仓为事实源)同步为同样的错误语义。
新增 TestLuaSyncInjectUnavailable 钉住不挂死。
|
2026-09-13 21:59:23 +08:00 |
|
|
|
52127a3323
|
fix(resident): CreateResident 注册入站 inputch 后加 defer 回滚
注册点与 residents 登记之间当前无可失败步骤,但缺回滚路径就是 child/<id>
残留那只 bug 的另一条入口。加 registered 标志 + defer:未走到成功返回就注销。
|
2026-09-13 21:59:10 +08:00 |
|
|
|
11d9038927
|
fix(io): ChannelRegistry 补 UnbindOutputTarget,Unregister 清理 outputTargets
outputTargets 只增不减:Unregister 一个 inputch 后,指向它的输出目标登记仍
留在表里,ResolveOutputTarget 会继续把消息路由到已不存在的 agent/inputch。
与刚修的驻留 inputch 残留同属「注册未注销」类。现补 UnbindOutputTarget
(幂等),并让 Unregister 顺手清掉显式绑定与同名回退两种目标登记。
|
2026-09-13 21:59:10 +08:00 |
|
|
|
a15e7c2dc1
|
chore(lint): 加 .golangci.yml(funlen/gocyclo/lll/dupl,warn-only)+ make lint-full
main 上曾有 4 个 >=300 行函数、12 个 >=200 行函数;没有复杂度 linter 是它们
长期存活的直接原因。先以 warn-only(issues.exit-code:0)立阈值、只出清单,
历史债下降后再收成硬门禁。测试与 testdata/third_party 排除长度类规则。
|
2026-09-13 21:59:10 +08:00 |
|
|
|
4518c3eb03
|
fix(lua): events.subscribe 改用内部 Subscribe + 订阅生命周期(修死锁/use-after-close)
上一版 Lua 对齐引入的 sdk.events.subscribe 有两个真问题,本提交修掉:
1) 用了公共 SDK 的 Events(),但本内核从未注入 event subscriber
(SetEventSubscriber 全仓无调用点),拿到永远是 nil ⇒ subscribe 只会
返回 "events unavailable"。改用内部 SDK 的 s.Subscribe——内置插件走的就是
这条路径(cli/webui/skillmgr 全用它)。
2) 自死锁:subscribe 会在 Lua 的 plugin.start(sdk) 回调里被调用,而
luaPlugin.Start 正持有 p.mu;原实现在 subscribe 里再 lock p.mu 追加 subs,
不可重入 ⇒ 测试实测 30s 超时。改用独立的 subsMu。
3) use-after-close:Stop 会 Close LState,但事件订阅此前无人取消,残留回调
再触发就会碰已关的 L。现在:Stop 先(不持 p.mu,避免与 Bus.Publish
锁序反转)取 subsMu 取消全部订阅,再置 closed 并关 L;事件回调持 p.mu 后
先查 closed,已进入等锁的旧回调会直接返回。
4) plugin_mgr 访问补 nil 保护(部分单测构造的 SDK 不含 pluginMgr)。
回归:TestLuaEventsSubscribeAndStopCleanup——订阅后 Publish 命中、Stop 后
再 Publish 不 panic。全套 Lua 测试在 -race 下通过。
|
2026-09-13 20:33:13 +08:00 |
|
|
|
44cb7243c5
|
fix(resident): 销毁驻留子时注销其入站 inputch(child/<id>)—— 修登记表脏数据累积
根因:residentInboundChannel 在 create 时把 child/<id> 登记进共享登记表
(Plugin=resident, Owner=父),但 teardownResident 只把划入的 inputch
(如 timer)归还为未分配,从未注销这条入站登记。于是每次 create/destroy
都在登记表里留下一条脏记录,且随次数单调累积。
实测(HomeAgent 侧,HΔ-Kernel v1.3.10 / 1b49365):
resident_agents destroy 之后,input_channels by_agent 仍列出 child/<id>,
归属 main;而 HomeAgent 没有任何工具能单独注销 inputch,只能重启 homed 清。
修法:teardownResident 里用纯函数 inboundChannelName 算出名字并 Unregister。
不能复用 residentInboundChannel——它有重新登记的副作用。
该路径同时覆盖 destroy / reclaim / StopResidents(父退出)。
测试:TestResident_LifecycleAndNoOrphans 增加两条断言——销毁后与父退出后
child/<id> 都必须从登记表消失。
|
2026-09-13 20:19:37 +08:00 |
|
|
|
162f33f81e
|
feat(lua): Lua 插件桥全量对齐 SDK 1.3.0(媒体/注入标志位/优先级/事件/通道注销)
内核 Lua 桥(internal/plugin/lua_plugin.go)此前停在 v0.8.0 时代能力面,
1.1/1.2/1.3 新增能力只在 Go 侧存在,而 PLUGIN_DEV.md 宣称『能力完全对齐』。
本补丁把 Lua 侧补齐到与公开 SDK 1.3.0 对齐:
- 1.1 媒体:memory.commit 支持 sentence_text/media_digests;
doc.insert_with_media + attachments;text_memory.append attachments;
set_tool_blocks / inject_input_media(_sync) / inject_interrupt_media。
- 1.2 注入语义:inject_input_sync(_opts)、六个 *_opts 变体
(no_memory/context_policy/cleaner_name/priority);
ToolDef/ChannelDef 解析 context_policy。
- 1.3 优先级与动态通道:priority 常量透传;unregister_output_channel。
- StageContext 暴露 reasoning_content/context_msgs/token_usage/memory/extra/errors。
- 新增 sdk.events.subscribe 与 sdk.plugin_mgr.*。
- sdk.lua mock 同步(单一事实源在 SDK 仓 sdk/lua/sdk.lua,内核副本由
third_party/homeagent-sdk/scripts/sync-lua-sdk.sh 同步)。
契约测试(lua_surface_test.go):
- 守住内核内嵌 mock 与 SDK 仓事实源一致;
- 守住 mock 承诺的每个函数都有运行时 RawSetString 绑定;
- 覆盖 opts/media/attachments 解析与 context_policy 透传。
文档:中英 PLUGIN_DEV.md 的 Lua API 表补齐并改为『对齐至 SDK 1.3.0』。
|
2026-09-13 19:56:32 +08:00 |
|
|
|
180b96e21a
|
docs: 记下 gitcode 附件的"同名只写一次"硬约束(校验和首次没传全就永远补不回来)
实测证据:同一个名字(ZZprobe.txt)传两次不同内容,下载端始终返回第一次那份;
资产列表里该名字只有一项。删除接口走不通 —— release JSON 不含 `id`,附件列表接口 404,
`DELETE .../releases/<tag>/attach_files/<name>` 返回 400「参数类型错误」(要数字 id)。
后果(1.3.1–1.3.10 全都踩了):首次上传 `SHA256SUMS` 时只有 linux/amd64 四个产物,
之后补 arm64/darwin/win 时"合并后重传"**全部无效** —— 线上那份至今仍是 4 项、带 `./` 前缀,
arm64/darwin/win 的产物没有校验依据。
⇒ 纪律:**打包全部平台后才第一次上传校验和**;分批上传时先传产物、最后传校验和,
校验和只传一次。补救只能换名(`SHA256SUMS.complete`)或重建 release(需重传全部产物)。
|
2026-09-13 18:50:06 +08:00 |
|
|
|
3fda1db9c0
|
fix(packaging): package-windows.sh 支持 NSI 覆盖 + payload 预检
上一步失败:`File "..\..\build\linux-payload\*.*" -> no files found`。
原因:NSIS 的 `File` 路径**相对 .nsi 所在目录**解析,而我用了主仓的 installer.nsi
(它去找主仓的 build/linux-payload),payload 却 stage 在 tag 的 worktree 里。
改法:`NSI` 可覆盖 —— 在 tag 的 worktree 里构建时用**该 tag 里的** installer.nsi
(与产物同源,也正是可复现发布该有的样子);另加 payload 空载荷预检(空载荷=装不上,
必须直接失败而不是打出一个没内容的安装器)。
|
2026-09-13 18:29:13 +08:00 |
|
|
|
8910c8c454
|
docs: 补两条流水线纪律(脚本必须 set -e;tag worktree 里的驱动脚本)
今天连踩两次,都是"脚本本身"的问题而不是打包逻辑的问题:
1. **没 `set -e`**:`package-windows.sh` 在 tag worktree 里找不到(新脚本只在 main),
bash 报 No such file or directory 之后**流程照旧往下走**,把只含 4 项的校验和
传上去覆盖了原本覆盖 10 项的那份 ⇒ 只能把产物下回来重建。
2. **驱动脚本不在 tag 里**:发布件在 tag 的干净 worktree 里构建,而刚补的脚本还没进 tag。
⇒ 让脚本支持 `DIST_LINUX` / `BUILD_DIR` / `DIST_RELEASE` 覆盖,用"主仓脚本 + 产物目录
指向 worktree"来解;文档写清这条约束。
同步进发布技能的同名小节(这两条和"分批上传要全量重算校验和"是同一类:**校验和的完整性
比产物本身更容易被流程吃掉**)。
|
2026-09-13 18:23:22 +08:00 |
|
|
|
c83eca5e88
|
fix(packaging): package-windows.sh 支持 DIST_LINUX/BUILD_DIR/DIST_RELEASE 覆盖
我在 v1.3.10 的 tag worktree 里调这个脚本,而它提交在 main(tag 里当然没有)⇒
`No such file or directory`;又因为脚本没加 `set -e`,它继续往下跑,把只含 4 项
(linux amd64)的校验和传上去,覆盖掉了原本覆盖 10 项的那份。
目录可覆盖后就能「用主仓脚本、产物目录指向 worktree」,两个坑一起消掉。
|
2026-09-13 18:22:33 +08:00 |
|
|
|
594496a225
|
feat(packaging): 补 Windows(WSL) 安装器的驱动脚本,并按变体定向 payload
用户要求:**Windows 的 homed 安装包应当是往 WSL 里安装**。口径本身早已落地
(installer.nsi 注释 + install-via-wsl.ps1 + build.sh 的 WSL 分支),但缺两样东西:
1. **没有驱动脚本**:`build.sh` 里没有 `makensis`,仓库里也没有任何脚本调用它 ——
build/ 下那几个历史 .exe 是手工打的。新增 `deploy/packaging/package-windows.sh
<server|client|full> [arch]`:按变体准备 payload、必要时编 waiter.exe、调 makensis、
把产物落到 dist/release。
2. **payload 不分变体**:`build.sh` 的 `stage_linux_payload` 把 dist/linux 下所有 deb+tar
全塞进 payload ⇒ 现在 server/full 的 deb 各带 ~719MB 模型,任何变体的安装器都会
膨胀到 ~2.4GB。而 WSL 侧脚本只取 payload 里的**第一个** `.deb`
(install-via-wsl.ps1:141)⇒ 按变体只放对应的那一个包。
同时:client/full 需要 Windows GUI payload(HAS_GUI=1),本机无 electron-builder 时
**明确失败并给出命令**,不产出"装完没有界面"的半残包。
docs/git-branching.md §七.5 补上这条口径与三条命令。
|
2026-09-13 18:13:04 +08:00 |
|
|
|
56ba1332f3
|
docs: 补「分批上传时后一轮必须全量重算 SHA256SUMS」
实测踩到:v1.3.10 先传 amd64 的 9 个资产(含只覆盖 amd64 的 SHA256SUMS),
后补 arm64 时按 arm64 那 4 个文件重算 ⇒ 同名附件覆盖 ⇒ amd64 的校验和消失。
校验和是附件的唯一完整性依据,丢了等于没有校验。已同步到技能的同名小节。
|
2026-09-13 17:35:02 +08:00 |
|
|
|
1d4f2beeea
|
fix(prompt): 去掉"每轮只能发一次 output_send"的凭空限制;type 缺省即 text
用户现场指出:**qq 插件的输出通道判据太严了**(那条判据在插件侧,已单独修:
`output_send__qq` 不再受"当前会话身份"限制)。同时内核提示词里还有一条**同类的凭空限制**:
「每轮对话**通常只需调用一次** output_send__{通道名} 即可完成回复。
仅在内容确实超过单条消息长度上限(如 >4000 字)时才拆分为多条」
可设计上输出是 agent 的**主动调用**:收到一次输入后,可以往**任意(已授权的)通道**
发**任意多次**(分段播报、先回执后结论、同时通知多个通道都合法)。这句话会让模型
自己收起合理的多次输出 —— 而且它不是任何机制的要求,只是当初为压 output-loop 写的
措辞(真正的防环机制是"回执只回 ok、不回传富结果",那条保留)。
改法:
- 提示词改为明确授权:**输出次数与目标通道由你自己决定**,没有「一轮只能发一次」的限制;
只保留两条真话:单条长度上限(超长拆完整段落)、别反复重发**完全相同**的内容。
- `output_send__*` 的 `type` 参数改为**可选**(缺省 text):判据该拦的是"不知道发什么",
不是"没写众所周知的默认值"——此前缺 type 会直接失败并让模型重试一次。
判据 3 条(新增 `output_rules_test.go`):提示词不得含输出次数限制且必须显式授权 /
省略 type 时按 text 发送成功且 schema 的 required 只有 payload / 空 payload 仍被拦。
(cherry picked from commit 17ea7fd5f0)
|
2026-09-13 16:04:16 +08:00 |
|
|
|
5160d8d4d1
|
chore(sdk-mirror): qq 插件 1.4.1(输出工具不再受当前会话身份限制)
镜像 SDK 仓的插件修复:被子的中断唤醒的一轮里,父带齐 meta 调 output_send__qq 也被
「可信 QQ 会话身份不完整」拒掉;输出改为先放行(目标由 meta 决定),读取类工具仍限当前会话。
|
2026-09-13 16:03:28 +08:00 |
|
|
|
e2500035d5
|
fix(resident): 子的「轮次」一直显示 0 —— info() 根本没填 Rounds
现象(用户线上联调实录 + 我复验):父侧 `resident_agents` 列出 `输入ch=[timer] 轮次=0
处理表=2` —— **处理表已有两条记录,轮次却是 0**,自相矛盾,容易被读成"子没干活"。
根因:`residentChild.info()` 构造 `ResidentInfo` 时**从来没有填过 Rounds 字段**
(结构体里有这个字段,于是永远输出零值),不是计数漏加。
改法:`Rounds = 已执行轮次数`(调度器执行计数,单调不减)。新增 `Agent.roundsExecuted()`
并写明为什么**不能**用 inputch 处理表条数当轮次:那张表记的是"当前上下文窗口内"的轮次,
压缩会清空(设计 §8.3)—— 用它会让父看到轮次倒退。
判据:inputch 路由测试里补一条断言 —— 子处理完输入后 `info().Rounds > 0`。
(cherry picked from commit cd88b2dfe5)
|
2026-09-13 15:41:07 +08:00 |
|
|
|
886ba78b11
|
fix(scheduler): inputch 划给子后输入只流向子 —— 补上「进内核之前」的输入路由
用户指出的语义(设计稿 §4.1 早已写明):
**inputch 是可分配资源**,「路由发生在**进内核之前**」—— 划给某个 agent 后,
该通道的输入**只流向那个 agent**;outputch 不同,授权是**非独占**的,
父依旧可以通过它发送内容。
而代码里 inputch 划拨只做了**登记**,没有做**路由**:
- 插件注入输入的 io 是**根 agent 的**(`cmd/homed` 里 `pluginReg.SetIOManager(iom)`);
- 唯一消费输入的是「该 io 自己的调度器」(`scheduler.go` 读 `a.io.InputChan()`);
- `ChannelRegistry.Assign` 只把 Owner 写进登记表,**没有任何转发动作**。
⇒ 现场表现(用户线上联调):子挂 `inputch=[timer]`,**timer 的输入却打在父身上**
(日志 `[agent] interrupt from timer/timer`),子侧 `轮次=0` 永远不动。
登记表里的 Owner 于是沦为标签。
改法(按 §4.1 把路由放回"进内核之前"):
- `IOManager` 增加 `InputRouter`(`SetInputRouter`),并把**五处直接入队**收口到
`deliverInput`:`InjectInput` / `InjectInputSync` / `InjectInputTo` /
`InjectInputSyncTo` / `InjectInterrupt`(排队与中断两条路都过路由)。
- 内核注入路由器 `Agent.routeInputByOwner`:查 inputch 的 Owner —— 归自己/未分配 ⇒
本内核处理;归自己的某个驻留子 ⇒ `DeliverRouted` 交给它(**不再进父的队列**);
归一个不存在的 agent ⇒ **不吞输入**,父兜底 + 留痕(吞掉输入比多处理一条更糟)。
- `DeliverRouted` 是"已路由"的投递口,不再二次路由(避免成环)。
- 同步输入的 `ResponseCh` 随事件一起走 ⇒ 回答由持有者写回同一回程(§4.3)。
判据(新增 6 条):
- io 层:被接管时排队/中断都**不入本内核队列**(且中断确实经过路由)/ 放行与未设
路由器时与历史行为一致 / `DeliverRouted` 不再触发路由
- 内核层:划给子的 inputch 输入进**子**(子 Executed>0)且**父 Enqueued 不变** /
归属到不存在的 agent 时父兜底(不吞)/ 未分配的 inputch 仍归父
(cherry picked from commit 4707b05498)
|
2026-09-13 15:35:59 +08:00 |
|
|
|
d0997f6279
|
docs(resident): 补写「子的 io 通道视图 = 对父的实时回退」(N3 落地口径)
输出通道在 io 层就是 Device,由插件登记在父的 IOManager 上;驻留子只共享了 inputch
登记表 ⇒ 子侧 childIO 空壳(现场:子调 output_send__cli 被判「通道不存在或不可用」)。
文档写清:继承方式(SetParentIO)、四个受影响的方法、为什么是实时回退而非快照、
以及回退只解决看得见、授权仍在白名单之后。附线上实测结果(result: ok)。
|
2026-09-13 15:16:58 +08:00 |
|
|
|
91c4bc01f1
|
fix(resident): 驻留子继承父的输出通道 —— 修「子侧 childIO 空壳、子不会发消息」
现场(用户在线上跑驻留子联调,日志实录):
父 agent 侧「通道装载完整」,子 `demo-resident` 侧 `childIO` 是**空壳**:
子的 `output_list_channels` 为空、`output_send__<通道>` 一律被判
「通道 [X] 不存在或不可用」,连 `output_send__*` 工具都不生成 ⇒ 子不会发消息。
根因:**输出通道在 io 层就是 Device**,而它们由插件登记在**父**的 `IOManager` 上。
`SpawnResident` 给子建的是全新 `IOManager`(它确实该有自己的输入入口与 outputCh),
却只共享了 inputch 登记表,**没有继承设备/输出通道视图**:
- `executeOutputSendTool` → `a.io.GetChannelCapabilities(ch)` 查的是 `devices[ch]` ⇒ 0
- 投递路径 `a.io.GetDevice(ch).Execute("output", …)` ⇒ nil
- 工具面 `tooldefs.go` 从 `a.io.ListChannels()` 生成 `output_send__*` ⇒ 空
改法:给 `IOManager` 增加**上级回退**(`SetParentIO`)——驻留子创建时把自己的 io 挂到
父的 io 上,`GetDevice` / `GetChannelCapabilities` / `ListChannels` / `ExecuteTool`
在自己没有时回退到上级。
为什么是**实时回退**而不是创建时复制快照:设备随资源生灭(远程设备上线/掉线以分钟计,
现场日志 60 秒一个来回),复制出来的表转瞬即过期;而回退永远与父一致。
**授权不受影响**:回退只解决"看得见",能不能用仍由各自的 `AllowedOutputs` 白名单把关
(`executeOutputSendTool` 的授权闸 + 工具生成时的过滤都在白名单之后);
自己的登记优先,子可以覆盖/屏蔽同名通道。
判据(新增 5 条):
- io 层:无上级时行为与以前完全一致 / 挂上级后看得见 / **实时**(父新登记立刻可见、
注销立刻不可见)/ 同名自己的优先且不重复列出 / `ExecuteTool` 同样回退
- 内核层:子看得见父通道 + 真能发出(父通道收到 1 次 output)/ 白名单外被拒且未送达 /
子工具面只生成授权通道(含 `_help`)/ 父后登记的通道立刻可见 / 默认即完整授权
(cherry picked from commit 8537577123)
|
2026-09-13 15:09:44 +08:00 |
|
|
|
fb2db2c304
|
docs: 两仓文档对齐当前版本状态 + 补发版产物清单;同步 SDK meta 路牌
用户指出:SDK 版本又带 patch 位、agent 自称 1.0.3、两个仓库文档都没更新。
docs/git-branching.md:
1. **§三 状态表**重写(停在 2026-09-12:main 还写 1.3.0、release/v1.2.x 还被当成"本条发布线、
尚无 tag"、SDK main 还写 1.2.0、完全没有 release/v1.3.x)。现按事实更新,并写明
`v1.3.0` 是已撤回的坏 tag。
2. **§2.3** 增补:发布线的 `meta.Version` 必须跟着该线已发的最后一个 patch 走;
只用 `-ldflags -X` 打版本而不改源码会让二进制与源码对不上账(1.3.1–1.3.4 就是这么打的)。
3. **反例表**补两行:人格文本在**播种时**固化版本(生产实例自称 v1.0.3)、
发布线路牌不随 patch 推进 / 给 SDK 误发 patch tag。并澄清"插值"必须在**渲染时**,
把算好的结果固化进配置库与写死没有区别。
4. **§七.5 新增"发版产物清单(可复现)"**:核心仓(tar.gz + 3 个 deb + SHA256SUMS,
校验和必须在全部产物生成后统一算)、SDK 仓(5 平台 hmapdev + SHA256SUMS)、
上传脚本的用法与两个坑(release 条目必须先存在;`hmapdev_*` 无扩展名不会被自动识别)。
起因就是本次"推了 tag 却没建 release、没打包"——推 tag ≠ 完成发版。
third_party/homeagent-sdk/meta/meta.go:镜像 SDK 仓 main 的路牌(1.3.0 → 1.4.0)与
版本语义注释更新(1.3.0 已定版 ⇒ 该号归发布线,main 推进)。
|
2026-09-13 14:41:24 +08:00 |
|
|
|
ba3ab5a7d9
|
chore(meta): main 的版本路牌推到 1.4.0(1.3.0 已归 release/v1.3.x 所有)
规范 §2.1 / §七.4:切出 release/v1.3.x 后,1.3.0 就归发布线所有,main 立即推进到
下一个未发布中版本。此前停在 1.3.0 属遗漏 —— 会让 main 构建出来的二进制自称已发布版本。
|
2026-09-13 14:34:46 +08:00 |
|
|
|
a4ebcb6e96
|
fix(config): 播种时不再把版本号写进人格文本 + 存量实例一次性去版本化
用户发现:agent 自报版本 **1.0.3**,内核早已 1.3.x。
根因(两层):
1. `SeedDefaults` 当年用 `fmt.Sprintf("…HΔ-Kernel v%s…", meta.Version)` **在播种时**
就把版本写进了 `core.agent.system_prompt` —— 装完即冻住,之后每次升级都不会
去改配置里的文本,于是实例终生自称装机那天的版本。默认模板用 meta.Version
插值本是"不写死"的做法,但**播种 = 把插值结果固化**,等于写死。
2. `core.agent.personal_prompt`(新人格机制)本身是对的(DefaultPersonaPrompt
无版本字面量,由 TestDefaultPersonaPromptHasNoVersionLiterals 钉住),
但旧键仍在系统提示词里说话,模型就照抄旧键的版本。
改动:
- **不再播种** `core.agent.system_prompt`:留空 → 组装时取 cmd/homed 的内置底座
提示词;人格由 personal_prompt 承载。全新安装不再预置会腐坏的文本。
- 新增 `migrateSeededSystemPrompt()`(在 SeedDefaults 最前,故不被播种标记早退):
只对"当年那段播种模板"(前缀 + `HΔ-Kernel v<数字>` 字面量双判据)做
`v<数字>` → `v{{kernel_version}}`;用户自己写的人格卡一律不碰。
一次性标记 `core.internal.system_prompt_deversion_v1` 守住幂等 ——
幂等语句不等于语义幂等,重复执行会把用户后来手写的版本号也改掉。
- 与 v1.3.5 的占位符展开配套:占位符在组装系统提示词时按真实构建展开。
回归判据 4 条(`prompt_migration_test.go`):存量卡被去版本化且正文不动 /
迁移只跑一次 / 用户自写卡不动 / 全新安装不播种该键。
|
2026-09-13 14:34:44 +08:00 |
|
|
|
f168862eaa
|
fix(prompt): 系统提示词支持版本占位符 —— 人格卡不再写死版本号
现象(用户发现):agent 向用户自报版本是 **1.0.3**,而内核早已 1.3.x。
根因:**人格卡是配置项**,线上 `core.agent.system_prompt` 里写死了
「HΔ-Kernel v1.0.3 型号的家政型 AI 管家助手」——那是当年装机的文本,
之后每次发版都不会去改它,模型于是照抄给用户。默认模板用 `meta.Version`
拼接(`registry.go` 的 `fmt.Sprintf`)所以一直是对的,**只要被自定义过就会漂**。
修法:在 `buildSystemPrompt` 组装处展开占位符,让这类文本跟随真实构建:
{{kernel_version}} → meta.Version(如 1.3.5)
{{kernel_commit}} → 构建 commit
{{sdk_version}} → 所兼容 SDK 版本(如 1.3.0)
- 未知占位符**原样保留**:写错了要看得见,而不是被静默换成空串;
- 不含 `{{` 时原样返回(提示词在热路径上);
- 覆盖所有路径:主 agent 与驻留子都经 `buildSystemPrompt`,
`persona_set` 写入的文本同样在读取时展开(存的是模板,不是渲染结果);
- 配置项描述里写明可用占位符,引导用户别再写死版本。
回归判据 `TestExpandPromptVars`:展开正确 / 内置占位符不残留 /
线上真实人格卡文本能被纠正 / 未知占位符不被吞 / 无占位符不改写。
|
2026-09-13 14:34:44 +08:00 |
|
|
|
44cc824b28
|
chore(sdk-mirror): a2a 1.3.0→1.3.1、acp 1.2.0→1.2.1 的 plg.json 跟上 SDK 仓
SDK 仓 11303e3 升了两个示例插件的版本(补 RegisterInputChannel 后按插件自身语义升 patch),
外层仓镜像里这两个 plg.json 忘了同步 —— 镜像与源头不一致会让"装的是哪个版本"对不上账。
|
2026-09-13 14:30:56 +08:00 |
|
|
|
937359b4df
|
feat(pluginmgr): plugin_install 支持本机 path(配合 plugindev_build 的产物)
背景:Agent 现在能自己构建插件了(plugindev 插件封装了 hmapdev),但安装只支持
http(s) URL —— 本地刚构建出来的 `dist/*.hmap` 装不上,链路断在最后一步。
pluginmgr 的 HTTP API 本来就接受 `{path}`(installFromPath),只是工具面没暴露。
改动:`plugin_install` 增加可选 `path`(本机 .hmap 路径),与 `url` 二选一,
同时给出时以 `path` 为准;`path` 必须存在且不是目录。描述里写明
「配合 plugindev_build 的产物用这个」。
于是 Agent 的完整闭环成立:
plugindev_init → plugindev_build → plugin_install(path) → plgreload
|
2026-09-13 14:17:11 +08:00 |
|
|
|
55dc6545f5
|
perf(memory): 静态词向量改用 float32 存储(省 ~0.65GB 常驻)
生产实测:`[static_embedder] loaded 200000 words`(zh) + `378151 words`(en) = 57.8 万词 × 300 维,
`map[string][]float64` 光向量本体就 **1.29GB**(外加 map 开销 ~0.1-0.2GB),占 homed
4.14GB RSS 的约三分之一。
源数据(fastText 文本格式)本身就是 float32 精度,用 float64 存没有任何收益:
- `words map[string][]float32` / `unkVec []float32`;
- 加载时按 `ParseFloat(..., 32)` 解析(与源精度一致);
- 相似度累加仍在 float64(`sum []float64`,读时提升),计算精度不受影响。
⇒ 向量本体 1.29GB → 0.65GB,**省 0.65GB**。(与配置侧 `#topN` 可叠加:
生产把两份 vec 各限 5 万词后,向量降到 ~0.22GB。)
防复发:`TestStaticEmbedder_VectorMemIsFloat32` 用**编译期类型断言**
(`var typed []float32 = vec`)+ 字节数断言(词数×维数×4)钉住 —— 改回 float64 会直接编译失败。
验证:`go test ./internal/memory/ ./internal/agent/core/ ./internal/nlp/` 全绿。
|
2026-09-13 14:09:35 +08:00 |
|
|
|
fddefc78a1
|
fix(plugin): "只声明出站通道"的告警改为插件加载完成后判定(此前按注册顺序误报 qq)
## 现象
生产日志(v1.3.1 启动)出现:
`[plugin] qq 只声明了输出通道 "qq",已按双向通道兜底登记 inputch;若要明确意图请显式 RegisterInputChannel`
用户据此问"qq 插件你没更新?"
## 查证:qq 没漏,是我的判据错了
- SDK 示例 `example/qq/plugin.go`:`RegisterOutputChannel("qq")` 在 368 行、
`RegisterInputChannel("qq", {NoMemory:true, Cleaner: inputCleaner})` 在 399 行 —— **先出站后入站**;
- 生产 `plugins/qq/plugin.bin`:版本 1.4.0,且二进制里含 `inputCleaner` 痕迹 ⇒ 确实调用了入站声明;
- 我的兜底告警在 **RegisterOutputChannel 的那一刻**判"有没有入站声明" ⇒ 对"先出站后入站"
这种完全合法的写法必然误报(a2a/acp/weather 同理)。
## 修法
告警判据从"注册时刻"改为"**插件 Start 结束后最终声明了什么**":
- `regOutput` 只保留兜底登记(功能不变),不再告警;
- 新增 `warnOutputOnlyChannels(plugin)`,在插件加载/重载成功后统一判定:
遍历该插件**最终**声明过的出站通道,只有始终没有对应入站声明的才告警,
且措辞改为"内核已兜底登记 inputch,若这是有意为之可忽略"。
- 判据与顺序解耦后,告警才代表真实缺口(例:weather 的 `weather_out` 与
`weather_in` 名字不同,出站名从未被声明为入站 —— 那条告警就是真的)。
## 验证
- 新增 `TestWarnOutputOnlyChannels`:①先出站后入站(qq 写法)**不告警**;
②只声明出站(weather 写法)**告警且只报那一个通道**。
- `go test ./internal/plugin/ ./internal/plugins/...` 全绿。
v1.3.2
|
2026-09-13 13:40:24 +08:00 |
|
|
|
3f431063e2
|
chore(sdk-mirror): 同步 SDK 仓 v1.3.1 的文档 —— 通道名会进 LLM 函数名(命名约束)
镜像文件:`third_party/homeagent-sdk/sdk/plugin.go`(`RegisterOutputChannel` 的命名约束)。
SDK 仓对应提交/tag:v1.3.1。
说明:内核 v1.3.1 的 tag 已指向功能修复提交 d17c186,本镜像提交在其之后 ——
文档镜像不参与二进制构建,故不影响已部署产物;功能与文档的对应关系见两仓 tag 说明。
|
2026-09-13 13:10:38 +08:00 |
|
|
|
18d7ad3a36
|
fix(remotedevice): 设备通道名改用 - 分隔并派生合规名(v1.3.0 部署后 agent 完全不应答的根因)
## 事故
v1.3.0 部署到生产后,**整个 agent 不应答**:任何对话都返回
`all 3 providers failed, last error: api error 403: model "claude-opus-5" is not allowed for this key`。
回滚到 1.2.2 立即恢复(部署前 403=0/成功对话=10,部署后 403=5/成功对话=0)。
## 根因(网关日志给出的原文)
```
tier 3 gozen/deepseek-v4.1-flash: api error 400: [invalid_request_error]
Invalid 'tools[299].function.name': string does not match pattern '^[a-zA...
```
设备的每设备输出通道名叫 `device/<id>`,内核按 `output_send__<通道名>` 生成工具 ⇒
`output_send__device/<id>` 里的 `/` 违反上游函数名规范 `^[a-zA-Z0-9_-]{1,64}$`。
上游不是"拒掉这一个工具",而是**整条请求 400** ⇒ 网关 auto tier 全链条失败
(400/429/503 混在一起)⇒ 内核只能报"所有 provider 都失败"。
两台真实设备(waiter-fnnas / waiter-mainnas)一上线就登记了这种通道,于是必然触发。
## 修法(改插件,不改内核)
初版我在内核里加了"通道名净化 + 反向解析"层。用户否掉了这个方向,理由对:
**通道名是插件自己的声明,不合契约就该改插件**,不该让内核替插件擦屁股。
内核侧改动已全部回退(HEAD 干净)。
插件侧两处:
1. 分隔符 `device/<id>` → `device-<id>`(源码与来源标签统一,不留两套名字)。
2. 设备 id 是**外部输入**(设备自己声明),可能含空格/非 ASCII/超长 ⇒
`deviceChannelName()` 把它派生为**合规且唯一**的通道名:
保留 `[A-Za-z0-9_-]`、其它折成 `-`、主体截断到 32 字符(预算 64 = 13+7+32+7+…)、
发生截断或撞名时追加 id 的 6 位短哈希。同一 id 恒定同名;真名仍用于路由与日志。
核心契约写进了插件注释与 SDK 文档(见 SDK 仓同批提交):名字若来自外部输入,
**在插件侧派生合规名**,内核不会替你净化。
## 验证
- 新增 `TestDeviceChannelNameIsLLMFunctionNameSafe`:恶意 id(空格/符号/非 ASCII/超长/
会折成同名的两个 id)都必须派生出**合法且互不重复**的通道名与工具名。
反向验证:把分隔符改回 `/` 即 FAIL。
- 生产两台设备派生结果:`device-waiter-fnnas`、`device-waiter-mainnas`
⇒工具名 `output_send__device-waiter-fnnas`(37 字符,合规)。
- 全量 `go test ./...` = 37 包 ok / 0 FAIL;`-race`(remotedevice + core)无 DATA RACE。
|
2026-09-13 13:10:38 +08:00 |
|
|
|
201a02839e
|
chore(meta): SDKCompatibleVersion 升到 1.3.0(内核已实现 SDK 1.3.0 的全部新增面)
SDK 1.3.0 相对 1.2.1 的公开面新增(`git diff v1.2.1..main -- sdk/` 逐条核对):
- `InjectOptions.Priority` + `PriorityL1/PriorityL2/PriorityL3/PriorityL4`
- `UnregisterOutputChannel` + `OutputChannelUnregistrar` + `SetOutputChannelUnregistrar`
内核侧两样都已实现,故按既有先例(d1959cb 随"实现 SDK 1.2.0 新增方法"同步声明)在此声明:
- Priority:`applyInjectOpts` 落 payload → 调度器 `interruptLevel` 按四级中断阶梯调度,
L4 归内核自身与内核级插件;
- UnregisterOutputChannel:插件登记表的 `regOutputUnreg` + io 设备表注销
(远程设备 `device/<id>` 掉线即注销,不留死通道)。
不调新面的存量插件照旧可用(新增方法由插件调用、内核实现),无需重编。
|
2026-09-13 12:46:31 +08:00 |
|
|
|
f0be8cbaa5
|
feat(remotedevice): 设备能力 outputch 化 —— 每设备一个 device/<id> 通道 + 设备指令类工具补授权闸
用户指出 remotedevice 的能力应当 outputch 化。查证后发现比"应当"更严重:
设备方向**根本没有出站实现**。
## 查到的三个缺口
1. `devicectlDevice` 一直声明 `OutputCapabilities() = CapStructured`(对外宣称可作输出目标),
但 `Execute` 的 switch 里**没有 "output" 分支** ⇒ `output_send__devicectl` 必然拿到
`unknown device tool output`,回模型"通过 [devicectl] 通道发送失败"。
2. 全插件没有 `RegisterOutputChannel`,也没有任何 EmitOutput/output_send 路径:
设备方向只有「工具(请求-响应)」与「设备→agent 注入」,**agent → 设备是断的**
(唯一的下行通道是个 HTTP 端点 `/api/v1/device/push`,不在 agent 的工具/通道模型里)。
3. 寻址是聚合的:所有设备共用一个名字 `devicectl`,没有 `device/<id>`;且设备 caps 只在
插件内部软检查(`SupportsTool`),**绕过**了内核的 `AllowedOutputs` 授权闸 ——
驻留子只要拿到 `device_ctl_cmdrun` 就能指挥**任意**设备。
## 按方向切分(不是一刀切)
**出站/消息类 → 每设备一个输出通道 `device/<id>`**(与入站同名):
- 上线注册、掉线注销(caps 由设备声明的 caps 映射:文本恒有;有屏→图/文件;
speaker→音频;可跑命令(cmd/cmdrun/cmdresult)或未声明已知能力→全能力,与
`deviceSupportsTool` 的旧设备兼容规则一致)。断连不注销会留下死通道骗模型。
- 于是自动获得:内核按 caps 在**发送前**拦(送图给纯文本音箱直接拒);
`output_list_channels` 能列出设备;`AllowedOutputs` 可按设备收窄给驻留子。
- 上下线钩子用 `Registry.SetPresenceHandler`(**同步回调**)而不是既有的 `ChangeChan`
(那是 select+default,缓冲满会丢事件;丢一次就留下死通道或漏注册)。
- `devicectl` 保留为聚合通道,并把它"声明了却不实现"的 output 补实:按
`meta.device_id`(或 meta 就是设备 id / args.device_id)路由;缺省时返回**可执行**的
报错(列出在线设备),而不是含糊失败。
**RPC 类保留为工具**(`devicedetect`/`screensee`/`computeruse`/`clipboard*`/
`device_ctl_status|cmdrun|cmdresult`):它们的返回值(图像/命令输出/状态)必须进模型
上下文,做成通道会丢掉这个语义。
**并给设备指令类工具补上同一道授权闸**(core/toolcall.go):`device_id` 指向的设备
必须是本 agent 被授权的 `device/<id>`。根 agent 默认完整授权 ⇒ 无行为变化;
驻留子收窄后,"拿到工具就能指挥任意设备"的缺口被堵上(新增 3 条 core 测试钉住)。
**设备端参考实现**(`internal/devicebridge/client/bridge.go` + waiter):新增 `op=push`
分发与 `OnPush` 回调(文本/结构化;二进制走既有 `cmd_speech_*` → `DataHandler`),
waiter 把它打到终端。
## 设计口径(用户当场纠偏,已写进代码注释与 harness README)
**主动转发只有 webui 与 cli 两个交互界面**(webui 订阅 EventAgentOutput 渲染气泡、
cli 用同步回程写回终端)。其它通道一律要求 agent **显式** `output_send__<通道>`。
我第一版给 remotedevice 加了 `EventAgentOutput` 订阅来自动回投设备——那是凭空造了
第三个转发者,违背"输出是 agent 的主动调用",已撤回(该测试一并删除)。
## 验收
- 单测:caps 映射词表;设备上线→注册通道(含同名 inputch)/掉线→注销;push 真落到
WS 设备;聚合通道 `devicectl` 的寻址(无 device_id 报可执行错误、按 meta 投递、
指定不存在设备报错);core 授权闸 3 例。
- 全量 `go test ./...` = 37 包 ok / 0 FAIL;`-race`(agent/plugins/plugin/devicebridge/sdk)干净。
- **真二进制端到端**(私有 netns + mock LLM + 真 WS 设备客户端 scripts/kernel-stress/devclient.py):
设备上线 → 通道表出现 `device/pydev-1`(caps=[text file image audio structured],由
`caps:["cmd"]` 映射)→ agent 经 `output_send__device/pydev-1` 主动发送 → 设备收到
`{"op":"push","payload":"内核推给你的消息","type":"text"}` → 设备掉线 → 通道从表中消失。
|
2026-09-13 12:23:28 +08:00 |
|
|
|
9f80289a89
|
test(kernel-stress): 把"真二进制压力测试"harness 固化进仓库(私有 netns + mock LLM + 多连接驱动)
本轮压测是从 /var/tmp 里现写脚本跑的,能复现才能长期用 —— 固化到这。
为什么要有它(与 go test 的分工):单测都在进程内、参数显式给,抓不到集成面问题;
这一轮它抓到了三个只有真二进制才暴露的问题(DataDir 漏接线、插件通道未登记为
inputch、create 后子不开工),以及两处生成器缺陷(旧 SDK 导致生成工程编译失败;
模板工程从不示范通道登记)。
- `launch.sh`:在**私有 netns** 里同时起 mock LLM 与内核实例。
必须隔离的原因:生产实例占着 *:8080/*:9890/*:9876 且插件设了 SO_REUSEADDR,
同机再起一个实例会在 127.0.0.1 上与之并存绑定(首次实测抢到 9890 约 1 分钟);
netns 里只有 lo,结构上不可能碰到生产端口。unix socket 是文件系统对象,
所以驱动脚本在 netns 外照样能连。
- `mockllm.py`:OpenAI 兼容 mock(可控延迟 + SSE 分块 + 按 `!resident`/`!notify`
标记回工具调用),让无外网也能跑、且**流式段可被中断**(压抢占路径的前提)。
注意必须实现 `do_HEAD`:内核探活用 HEAD,501 会被判不可达 → degraded → rollback 循环。
- `stress.py`:多并发连接轰炸(单连接是串行的,造不出队列压力)+ 中断线程 + 峰值采样。
- `kcli.py`:CLI socket 客户端(`/auth` → `/kernel` / 广播)。
- README:前置配置(含探活端点、rollback 关掉)、用法、指标读法、已知坑。
实测结果(详见 6e60351 提交信息):密集 274 任务 executed=274/rejected=0;
稀疏中断下 suspended=resumed=preempted=27;驻留子全链路 + 优雅退出不留孤儿。
|
2026-09-13 11:38:31 +08:00 |
|
|
|
ae9b06976c
|
chore(sdk-mirror): 同步 SDK 仓 4cb3a0b —— 通道方向契约 + 示例插件显式登记 inputch
镜像文件(外层仓按策略只跟这些;`example/` 被 .gitignore 忽略,已跟踪文件用 -f 提交):
- `sdk/plugin.go`:RegisterInputChannel/RegisterOutputChannel 的方向契约文档
(入站 vs 出站分开登记;用哪个通道名注入就要登记哪个)。
- `example/{a2a,acp,browser,memo,calendar,rss}/plugin.go`:补 RegisterInputChannel。
未镜像(外层 .gitignore 忽略,按策略不入镜像):
`README.md`(README*)、`tools/hmapdev/{templates.go,cmd_sdk.go,cmd_build.go}`(tools/)
—— 即"模板工程"与"`sdk install --from`"两项改动只存在于 SDK 仓,详见该仓提交 4cb3a0b。
注:这 6 个示例此前未 gofmt(`example/*` 共有 9 个未格式化文件),
本次改动文件被 gofmt 顺带规范,diff 里含格式噪音(calendar 49 行、a2a 33 行)。
|
2026-09-13 11:38:10 +08:00 |
|
|
|
6e6035141a
|
fix(resident/plugin): 二进制级压测暴露的三个真问题(DataDir 漏接线 / inputch 未登记 / create 不开工)+ 通道双向登记贯穿全部内建插件
在真实内核二进制(私有 netns + mock LLM + CLI unix socket)上做压力测试时,
下面三个问题**只有跑真二进制才暴露** —— 单元测试里都显式传了参数、没走插件加载,
所以全绿也照样漏。
## ① 根 agent 的 DataDir 没接线 ⇒ 驻留子永远建不出来
现象:模型调用 `resident_agents` 成功,但结果是
`创建驻留子需要 data_dir 或显式 temp_path`。
根因:`cmd/homed/main.go` 构造 AgentConfig 时没有 `DataDir`,
而驻留子的 temp 图库需要 `<data>/residents/<id>/graph.db` 这个锚点。
(单测里 `AgentConfig{DataDir: dir}` 显式给了,所以测不出来。)
修:main.go 接线 `DataDir: cfg.Daemon.DataDir`;并在工具层加**兜底 + 告警** ——
data_dir 为空时从主图库路径反推(`<data>/memory/graph.db` ⇒ `<data>`),
失败才报错。静默失败会让线上表现成"工具能调但永远建不出来"。
## ② 插件通道没登记为 inputch ⇒ "划入 inputch"必然失败
现象:`划入 inputch cli: inputch 未注册`。
根因:`cli` 插件只调 `RegisterOutputChannel("cli", ...)`,却用同一个名字
`InjectTextSync("cli", ...)` 注入输入 —— 内核 inputch 登记表里根本没有它。
(实测审计:内建 6 个插件里只有 0 个登记过入站通道;SDK 示例里只有 qq/weather 是对的。)
修两处:
- **全部内建插件显式登记入站通道**:`cli`/`agentcli`/`timer`/`webui`(+`http`, NoMemory)/
`clawhubadapter`(每个 OC 通道声明处)/`remotedevice`(`device/<id>` 懒登记,幂等)。
- `registry.go` 把隐式兜底改成**留痕的兼容网**:只有当该名字还没登记为 inputch 时
才兜底登记,并打日志说明"建议显式 RegisterInputChannel"。
实测:改完内建插件后,启动日志里兜底告警 **0 次**。
## ③ create 之后子不开工 ⇒ rounds 恒为 0
现象:`[agent] r1 started, waiting for IO interrupts` 之后什么都没有,登记表里 rounds=0。
根因:`TaskPrompt` 只进了子的**系统提示词**,从没作为输入投给子。
修:create 即开工 —— 把任务提示词作为**第一条排队输入**投给子(排队而非中断:
创建是"安排工作",不是"打断它正在做的事")。
## 测试
- `TestResident_InputchTableAutoAndProactive` / `TestLightKernel_TraditionalContextNoTrimming`
随行为更新:create 会多跑一轮(任务提示词那轮也会写处理表),
断言改为"以创建时的表长为基线 + 等待新的一轮"。
- 全量 `go test ./...` = 37 包 ok / 0 FAIL;`-race`(agent/plugin/plugins)干净。
## 真实二进制压力测试结果(修复后)
私有 netns 里跑 mock LLM + 内核,用 CLI socket 驱动多并发连接:
- 密集:16 连接×12 输入 + 4 线程×20 次 L4 中断 → **274 任务 executed=274 / rejected=0 / errors=0**,
峰值排队 15、峰值待处理中断 76;
- 稀疏(中断每 3s 一次,压在排队任务的流式段上)→ **suspended=27 / resumed=27 / preempted=27**;
- 驻留子全链路:父建子(inputchs=["cli"])→ 子开工 → 子 `notify_parent` → 父侧收到
`interrupt from r1/child/r1`(L3,且父被抢占 suspended/resumed=1);
- 优雅退出:SIGTERM 后驻留子 temp 目录被清除、无残留进程。
|
2026-09-13 11:38:03 +08:00 |
|
|
|
aba1770701
|
fix(lightkernel): 传统上下文落到实处 —— 子不做策略性裁剪(并修掉裁剪估算的字节/rune 单位混用)
用户指出实现不自洽:"子 agent 是传统上下文,没有裁剪"。查证属实:子仍走
`formatMergedTimeline` 的预算裁剪、也可能走 `pruneOnInput`(既裁剪又向 doc 记忆归档)。
即"动态上下文"(父专属能力)漏进了轻量内核。
## 三处闸门(按 isLightKernel() 判)
1. **拼装预算**:新增 `contextTokenBudget(b)` —— 父用动态上下文的 `ContextTokens`,
子用**整个窗口** `MaxContext`。两个调用点都改(`stepPrepare`、`rebaseFramePrefix`;
漏掉后者时 prepare 之后的重建仍会裁,实测就是这么被抓出来的)。
2. **pruneOnInput**:轻量内核前置返回 —— 不做按相关度裁剪、不向 doc 记忆归档。
3. doc 记忆 / 整理流水线:早已由 `a.memory == nil` 关闭(N2c)。
## "不裁"的准确含义
不做**策略性**裁剪(不按相关度挑、不归档),只受"模型能收多少"这个硬上限约束;
且在撞到硬上限之前,contextfull(90% 窗口)已按 L4 上报父 agent ⇒
**丢事件的决定权在父**(压缩/回收/销毁),不在内核。文档 §16.0.0 记录三处闸门表。
## 顺带修掉的既有 bug(单位混用)
`formatMergedTimeline` 逐事件估算原来是 `len(e.Source)+len(e.Input)+40` 再 ×2 ——
`len()` 是**字节**,而 `EstimateTokens` 是 rune×2 ⇒ 中文事件被高估 3 倍:
实测 2384 字的中文事件被估成 14398 token > 8192,于是窗口还有余量也提前 break、
把更早的事件整段丢掉。改为统一走 `EstimateTokens`(对父同样生效:中文长会话不再被过早裁剪)。
## 测试
- 新增 `TestLightKernel_TraditionalContextNoTrimming`:用**记录模型实收消息**的 provider
断言"更早的事件仍在"(为什么不看 TaskFrame:`prepareInputTask` 只做前半段,
消息在 `runTaskSteps`/`stepPrepare` 才拼出来 —— 我第一版断言就打在了空帧上);
填充量取"超过动态份额、但仍在窗口内",从而能区分父/子两种行为。
- 新增 `TestFullKernel_StillUsesDynamicContext` 作对照(父仍用动态份额)。
全量 go test ./... 37 包 ok / 0 FAIL;-race 干净。
|
2026-09-13 10:36:04 +08:00 |
|
|
|
cba33d2a6f
|
test(resident): 压力规模可用环境变量放大(RESIDENT_STRESS_N / RESIDENT_STRESS_ROUNDS)
默认仍是 8 子 × 12 轮(CI 友好);要跑加重压力就放大:
RESIDENT_STRESS_N=64 RESIDENT_STRESS_ROUNDS=40 go test -race -count=2 ./internal/agent/core/ -run TestResident_E2EAndStress
实测已跑:
- 24 子 × 25 轮(-race):通过
- 64 子 × 40 轮(-race -count=2,即 5120 轮次):通过
|
2026-09-13 10:25:49 +08:00 |
|
|
|
2ebbdadcd6
|
feat(resident): N3–N7 驻留式子 agent 全量落地(生命周期/双向投递/处理表/contextfull/e2e+压力)
设计:docs/zh/resident-subagent-design.md §6/§7/§8/§9/§10。
## N3 生命周期(resident.go)
- `SpawnResident`:主库**受限句柄** + 自己的 temp 实例(`LightMemory`)⇒ 子的轻量内核;
划入 inputch(登记归属)、授权输出通道、注入任务提示词;为父登记 `child/<id>` 入站 inputch;
建独立 `IOManager`(共享通道登记表);启动子。
- `DestroyResident`:停子内核、归还划入的 inputch(回到未分配)、丢弃 temp 目录、出登记表。
- `Stop()` → `StopResidents()`:**父退出必须销毁全部子、不留孤儿**(设计 §10 硬约束)。
- `Residents()` 登记表快照;`ResidentTable(id)` 父 pull 子的处理表(不打断)。
## N4 跨 agent 投递
- 子→父:`notify_parent` → 投进父的 `child/<id>` inputch,优先级 **L3**。
- 父→子:`SendToResident` → 投进子的 inputch,优先级 **L4**;
`isKernelLevelSource` 泛化为"该 agent 的上级"(`AgentConfig.KernelSource`)⇒
只有父能在子的阶梯上产生 L4(子内部一律 ≤L3)。
- 子的 contextfull → 父侧 `raiseKernelInterrupt`(内核级事件,带子标识,父侧 L4)。
## N5 inputch 处理表
- 子持有;`inputch_note` 主动写**优先**,轮末 `autoRecordInputch` 兜底 ⇒ 每轮必有记录。
- 压缩时清表(表记的是被压掉那段窗口的逐轮处理)。
## N6 contextfull(判据修正)
❗初版判据是"拼好的 `f.Msgs` 估算 > 90% 窗口",**结构上永不成立**:
`buildMessages` 拿到的 `budget.ContextTokens` 由 `targetUsage = 0.8 × 窗口` 推出,
时间线**在拼进消息之前就被预算裁过**,`f.Msgs` 封顶在 ~80% 窗口。
(初版测试用一个比系统提示词还小的窗口才勉强越线 —— 那等于什么都没测。)
现判据 = **未裁剪的积累上下文**(`a.context.Recent(0)`)超过窗口 90%:
它超过就说明下一轮必须丢事件,这正是"上下文满"。
三处置:`CompressResident`(保留语义:`TrimKeepRecent` 保留最近 N 条 + 清表)/
`ReclaimResident`(取消语义:`ExportTriples` 读 temp → 父选出要保留的 → `Commit` 进 main → 取消该子)/
`DestroyResident`(立刻销毁并移除)。
## 工具面
`resident_agents`(父,单工具多动作:list/create/send/inspect/compress/reclaim/destroy)、
`notify_parent` + `inputch_note`(子)。声明条件式:父才有前者,子才有后两者。
## 验收
`resident_test.go` 五项:生命周期与不留孤儿、双向投递(含"子的主动消息不得以 L4 出现")、
处理表(自动写 vs 主动写优先)、contextfull + 三处置、
**压力 8 子 × 12 轮(父→子 L4 与普通输入各半)+ 双向汇报 + 父退出清理**。
全仓 go test ./... 37 包 ok / 0 FAIL;`-race`(agent/memory/plugin)干净;
压力 `-race -count=3` 通过。
|
2026-09-13 10:20:06 +08:00 |
|
|
|
48cfa8fb6c
|
feat(lightkernel): N2c —— 轻量内核 profile(窄接口 GraphMemory + nil 即禁用整理面)
按用户指出的关键点(a.memory 多数使用点属"主 agent 整理记忆"与"记忆整理流水线",
子不该有那些路径)实现,方案见设计 §16.0。
## 窄接口:只有 Recall + Commit
新增 `GraphMemory` 接口(memoryface.go)—— 按调用方实测分类后,真正"根与子都要"的只有这两个:
- `Recall`:上下文检索 / memory_recall
- `Commit`:自动写入路径的图部分 / memory_commit
新增 `Agent.graph`(共同面)与 `Agent.graphMem()` 访问器:
- `graph` 显式为 nil 时**回落**到 `memory` ⇒ 既有"只用 Agent 字面量设 memory"的测试无需改动
(原本会出现"必须同时设两个字段"的脚坑,实测踩到后去掉了)
- 轻量内核:`graph = *memory.LightMemory`,`memory = nil`
## nil 即禁用:整理面自动消失,不需要受限包装
子的 `a.memory == nil` ⇒ 既有的 22 处 `if a.memory != nil` 关卡自动禁掉全部整理面:
- 记忆整理流水线(distill.go 的 archive/review/merge 循环)
- 记忆块 + 媒体桥(graphmedia.go / medialoop.go)
- 记忆整理工具(memory_merge / memory_delete_entity / memory_block_merge /
memory_purge / memory_edit / memory_introspect)—— 它们本就在 `if a.memory != nil` 块内
唯一拆开的一处是自动写入 `commitTriplesWithMedia`:
图部分走 `graphMem().Commit`(父落 main、子落 temp),块/媒体部分仍由 `a.memory != nil` 守卫。
`executeMemoryTool` 的读路径改走 `graphMem().Recall`;整理类 case 加 `requireFull()` 闸门,
被直调时明确报"本 agent 是轻量内核:记忆整理不可用",不静默降级。
## 验收(3 项新测试)
- `TestLightProfile_MemoryFaceWiring`:整理面为 nil、写入只落 temp(主库无子痕迹)、
读是并集(主库实体 + temp 实体都看得到)
- `TestLightProfile_OrganizeToolsAbsentAndRefused`:整理类工具**不进工具表**;
即便被直调也明确报"轻量内核不支持"
- `TestFullProfile_KeepsOrganizeFace`:对照,根 agent 仍保留整理面与整理工具
全仓 go test ./... 37 包 ok / 0 FAIL;-race(agent/memory)干净;gofmt 干净。
|
2026-09-13 10:02:02 +08:00 |
|