Commit Graph

386 Commits

Author SHA1 Message Date
0b1c201b5c docs: README 更新到 v1.2.0,并纠正已在 v1.2.0 删除的机制
README 的「项目状态」停在 **v1.1.1**,而且其中两条描述与当前实现**相反**:
「媒体以 `[<mime> <短digest>] <描述>` 标记存在于纯文本记忆中,描述是可检索的
语义记忆」与「引用计数式 GC(有引用者绝不删)」——这两套机制正是 v1.2.0 拆掉的。

本次不重写历史条目(它们记录了演进),而是:

1. 「项目状态」顶部新增 **v1.2.0** 条目:统一多模态向量空间(模型中立 SPI +
   Chinese-CLIP 默认,含「纯文本语义弱于 MLLM 型嵌入器」这一已知代价)、
   媒体升为图记忆一等节点(并写明具体删掉了什么)、数据面全量走共享内存
   (协议 2、不支持滚动升级)、注入标志位、发行包默认启用 ONNX 且随包模型、
   homed 改走 WSL2、以及三个安装链静默失败的修复。
2. 在 v1.2.0 条目后加一条显式提示:下方历史条目中的「描述式索引」与
   「媒体引用计数式 GC」**已在 v1.2.0 移除**——避免读者按旧文档理解现行行为。
3. 「下载」:Windows 安装器的版本号改到 v1.2.0,并写明自 v1.2.0 起因 homed
   不再支持 Windows 原生,安装器改为引导到 WSL2 并在其中按 Linux 方式安装;
   同时注明安装器含 AGPL 许可页。

中英双份同步。SDK 仓 README 另在 SDK 仓提交(d893bfa)。
2026-09-12 09:27:28 +08:00
5e10f012cb docs(license): 核心仓采用 AGPL-3.0-only,并写进包内与安装器
本仓此前**没有任何许可文件**,README 里也没有许可声明,而 rpm 元数据里甚至写着
`--license "Proprietary"`(与我们实际的分发意图相反)。

## 选择了什么

`LICENSE`:GNU Affero 通用公共许可证第 3 版官方全文(gnu.org 正本,
661 行 / 34523 字节,
sha256 0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0)。

选 AGPL-3.0-only 的理由:GPL 家族里**传染性最强**的一档,并且不允许选后续版本。
它比 GPL-3.0 多出 §13(Remote Network Interaction)——通过网络提供服务时也要向
使用者提供源码。这正是「最严格」在 GPL 家族里的落点。

依赖许可已核对为全部宽松且兼容:go-sqlite3 / gojieba / gopher-lua / bubbletea /
bubbles / lipgloss / yaml.v3(MIT)、golang.org/x/{sys,text}(BSD-3)、
Chinese-CLIP 产物(Apache-2.0,与 GPLv3+/AGPLv3 双向兼容)、ONNX Runtime(MIT)。
没有 GPL-2.0-only 这类与 AGPL 不兼容的依赖。

## 落在哪些地方

- `README.md` / `README_EN.md`:新增「许可 / License」章节,写明 §13 的含义、
  插件因**静态链接 SDK 源码**而成为衍生作品须同许可发布、以及随包第三方组件清单
- `deploy/packaging/package-linux.sh`:
  · 新增 `stage_license()`,**四个变体(full/server/client/tar)全带**
    `/usr/share/doc/homeagent/{LICENSE,copyright}`(copyright 为 DEP-5 机器可读格式,
    含第三方条目)
  · fpm 的 `--license "Proprietary"` → `"AGPL-3.0-only"`
- `deploy/packaging/installer.nsi`:新增 MUI 许可页
  (`..\..\LICENSE`,NSIS 以 .nsi 所在目录解析相对路径)
- `third_party/homeagent-sdk/LICENSE`:vendored SDK 的许可一并入库 —— 本仓
  `.gitignore` 有意不镜像 SDK 的 README/tools/package/example,但依赖的许可
  应当随依赖可见

## 待办(下一步)

README 的「项目状态」仍停在 v1.1.1,且写着已被 v1.2.0 **删除**的机制
(引用计数式 GC、`[<mime> <digest>] <描述>` 描述式索引)——单独一个提交修。
2026-09-12 09:17:03 +08:00
727768e083 docs(git): 修正上一提交的错字(但声两仓 → 但两仓) 2026-09-12 09:01:59 +08:00
0247206dfe docs(git): 写清「两仓 main 同步推进」的前提,修正 SDK 路牌
§七.4 原来只说「在 1.1.x 线发布期间,两仓 main 上的值都是 1.2.0」,容易被读成
「两仓 main 永远同值」,我正是据此把 SDK main 也推到了 1.3.0(已回退为 1.2.0)。

补写前提:推进以**该中版本已正式发布**为条件。
- 核心切出 release/v1.2.x 后 1.2.0 归发布线所有 → main 立即到 1.3.0(beta 也算占号)
- SDK 因 §七.2(beta 不发 SDK)要等核心正式 tag 才定版 → 在那之前 main 停在 1.2.0

并明确:**此阶段核心 main(1.3.0) 与 SDK main(1.2.0) 故意不对称**,
不是遗漏同步。§三 的 SDK 表行同步修正。
2026-09-12 09:01:34 +08:00
fbd3adabea fix(sdk-mirror): vendored SDK 路牌回到 1.2.0(SDK 不跟 beta 发版)
SDK 仓 44bd915 把 main 的 meta.Version 推到 1.3.0 是错的(12cabcb 已改回 1.2.0)。
按 §七.2,beta 不伴随 SDK 发版:SDK 1.2.0 要等核心的**正式** tag 才定版打 tag
(§七.3),在那之前 1.2.0 仍是 SDK 尚未发布的中版本,路牌不得越过它。

核心 main 的 1.3.0 不受影响(1.2.0 已归 release/v1.2.x 所有),两仓在此阶段
故意不对称——这一点已写进 SDK meta.go 的注释,避免再被「对齐」回去。

本提交只同步 core 里跟踪的镜像:sdk/ 目录与 SDK 仓仍然逐字节一致。
2026-09-12 09:00:41 +08:00
f778c613f1 docs(git): 1.2.x tag 历史补上 v1.2.0-beta.1
release/v1.2.x 已按 §2.4 走出 beta 通道:tag v1.2.0-beta.1(提交 215804c),
注释里写明通道、不兼容点与验收证据;beta 阶段不发 SDK(§七.2)。
正式 tag 待试运行无回退问题后再打,届时同步 SDK 仓。

同时把 tag 名资产整理记录在案:历史大写 tag V0.8.0 / V0.7.1 已按用户确认删除
(提交本身未受影响),现在主仓 tag 全部符合 SemVer 小写 v 约定。
2026-09-12 08:54:09 +08:00
7a418346a4 build(packaging): 统一 -buildvcs=false,版本/提交只认 ldflags 注入
发布分支的产物上出现了 `vcs.revision=1715b5c`——一个本机任何仓库都不存在的提交。

原因:VCS 信息**不进 build cache key**(Go 文档明确说明 VCS 变化不会触发重建),
命中缓存时会把上一次的 revision 一并带回来。

而 build.sh 本来就用 ldflags 注入 meta.Version / meta.Commit(权威来源),
所以这个额外信号既不可靠又会误导溯源:拿 `go version -m` 去查源码提交,会指向
一个幽灵提交——正是本项目一直在治的「静默不一致」。

处置:三处 go build 统一 `-buildvcs=false`,并在 LDFLAGS 旁写明溯源方法
(`strings homed | grep -x '<短 hash>'`,meta.Commit 是字符串常量)。

验证:重新构建 homed linux/amd64 →
  · `go version -m` 中 vcs.revision 0 处(此前 1 处且是错误值)
  · strings 中恰好 1 处等于当前 HEAD 短 hash
  · `-tags=onnxruntime` 仍在
2026-09-12 08:42:18 +08:00
a070cb563d docs(git): 分支对齐更新到 2026-09-12(1.2.x 线开启)
- main 路牌调到 1.3.0;release/v1.2.x 承载 1.2.0(vendored SDK 定版 1.2.0)
- release/v1.1.x 按 §2.6 退役(保留供追溯);两个历史 feature 分支
  (memory-media / plugin-proc-migration)已从远端删除,旧表待删项清掉
- 新增「1.2.x 发布线 tag 历史」表(当前尚无 tag),并写明它与存量插件
  **不兼容**(RPC 协议 2、fd3 布局改变、不支持滚动升级)以及
  **不满足 §2.4 跳级条件**的理由(改动面大,非单点修复)
- SDK 仓的 release/v1.2.x 尚未创建:按 §七.3 随核心**正式** tag 一起做
2026-09-12 08:39:05 +08:00
685ed7e4a0 fix(knowledge): 覆盖同名条目时摘掉旧向量
从一次真实的知识库更新里发现:在线实例更新一个已有条目之后,
knowledge_count=32 而 vector_count=33——多出来的那一条是上一版的副本。

成因:vector.Store.Insert 是**追加**语义(s.docs = append + index.Add),不按 id 去重;
而 Store.Add 走的是「写 content.md + 覆盖 items[id] + Insert 向量」。
文件与内存条目都被正确替换了,只有向量索引多留了一份。

危害不在于多占内存:**检索可能命中已被替换掉的旧内容**,而且完全静默——
条目数看起来是对的,只有向量数比条目数多。

修法:Insert 之前先 s.vec.Remove(id)(Remove 已按 id 过滤 docs 与倒排索引)。
回归测试 TestAddOverwriteReplacesVector 钉住 knowledge_count / vector_count /
content.md 三者都必须只剩新版。

注:该文件在 origin/main 上本就有 32 行 gofmt 差异(结构体字段注释对齐),
不属本次改动,按纪律不做整体重排。
2026-09-12 08:30:22 +08:00
dd99105d66 fix(build): GUI 输出目录用 --config.directories.output,-o 是 --mac 的别名
electron-builder 的 `-o` 是 `--mac`/`--macos` 的短别名(见 --help 的
Building 段),不是 output。于是 `-o "$BUILD_DIR"` 被当成 macOS 的 target
列表,报:

  ⨯ Unknown target: /home/program/trueagent/build

路径被 lowercase 后去匹配 target 名表,所以错误信息里的路径是全小写的
——这也是它看起来像「路径错」而实际是「参数位置错」的原因,v1.0.1 与
v1.0.3 两次发布都因此手工组装过 GUI。

改用 --config.directories.output=<dir>,已实测确认产物落在指定目录。

同时把 GUI 构建失败降级为警告:homed/waiter/initconfig 是发布主体,
而 GUI 依赖 electron 运行时下载(离线机器、arm64 缺缓存都会失败)。
set -euo pipefail 下不接住的话,一个可选组件会让整轮跨平台构建全废——
v1.0.3 就是这样只产出了 linux/amd64 三个二进制、arm64 与 windows
压根没跑到。
2026-09-12 08:26:12 +08:00
1fb5700662 chore(meta): main 的版本路牌推到 1.3.0
按 docs/git-branching.md §2.1,main 的 meta.Version 始终是**下一个未发布中版本**。
1.2.x 线已开(release/v1.2.x 承载 1.2.0),所以 main 指向 1.3.0。

它标记「main 正在积攒 1.3 的东西」,不表示 1.3.0 已经存在——1.3.0 没有任何 tag。
1.2.0 已由 release/v1.2.x 承载;main 不能再挂着 1.2.0,否则 main 的版本号
就与某一次发布的版本号相同,违反 §2.1「main 的版本号不是任何一次发布的版本号」。

两仓同步:SDK 仓 main 也在同一时刻推到 1.3.0(commit 44bd915)——SDK 版本跟随
核心中版本(§七.1),且两仓 main 都表示下一个未发布中版本(§七.4)。
本 commit 同时把 vendored SDK 镜像(third_party/homeagent-sdk/meta/meta.go)
更新到与 SDK 仓一致。

SDKCompatibleVersion 保持 1.2.0:那是本内核**实际实现并兼容的最高 SDK 接口版本**,
与「下一个未发布中版本」是两件事(参见 e4be966 时 main 的 1.2.0 / SDK 兼容 1.1.0)。

**此 commit 不 cherry-pick 到发布分支**(§五:版本号 bump 不跨分支搬)。
2026-09-12 08:25:30 +08:00
f1091676f8 style: gofmt 两处对齐(graphmedia_test.go 注释、tfidf.go 结构体字段)
这两处是本次特性分支带入的格式回归(origin/main 上不脏)。
纯空白/对齐调整,无语义变化;不整体重排文件。
2026-09-12 08:23:30 +08:00
eb4762a2b8 merge: 多模态统一向量空间与子进程数据面收敛(feature/multimodal-embedding)
合入 43 个提交,主线内容:
- 统一多模态向量空间:公共 provider SPI(pkg/embedding)+ 注册表,
  chineseclip 为 text+image 默认空间(512 维,Apache-2.0),qwen3vl 保留
- 共享内存接管全部数据面:工具调用帧、Cleaner、输入/输出 lane、媒体块、
  文档/知识正文;RPC 只传偏移描述符(协议版本 2)
- 图记忆原生媒体节点:媒体是一等节点与边,删除 media_refs 与描述式索引
- 注入行为可声明 no_memory / context_policy(默认不裁剪)
- SDK 1.2.0:注入标志位纯追加 + 示例 hmap 随发版
- 发行包默认启用 ONNX 向量空间;本批起模型与运行库随 server/full 包发布
- homed 放弃 Windows 原生支持,改走 WSL2
2026-09-12 08:21:34 +08:00
4d2028fbef chore(sdk): 同步 vendored SDK 镜像到 1.2.0
core 仓里跟踪的 SDK 镜像落后于 third_party/homeagent-sdk(那是个独立仓库):
已提交的 sdk/memory.go 仍带 MediaAttachment.Description,而 SDK 仓 b2eafdf 已把它
删掉(媒体不再以文本描述参与索引)。磁盘上的文件其实就是 SDK 仓当前的版本,
只是 core 的索引没有跟上——于是**干净检出 main 拿到的是一个与它构建所用 SDK
不一致的镜像**。

clean worktree 能编译(内核已不再引用 Description),所以这个不一致不会被构建
发现,只能靠比对发现——属于本项目一直在治的「静默不一致」。

同步内容:sdk/memory.go 与 SDK 仓 HEAD 逐字节一致。
2026-09-12 08:21:21 +08:00
d4c5e808c7 feat(packaging): 模型与 ONNX Runtime 随 server/full 包发布
模型与运行库是发行版能力的一部分,不做成「装完再自己下载」:

- package-linux.sh:新增 stage_multimodal_assets(),打 server/full 前校验产物
  SHA256SUMS、逐文件非空、运行库架构与目标一致,缺一即失败;client 包不含。
  顺带修掉三个让打包在最后一步才炸的既有缺陷:
  · 版本串直接取 git describe(v1.0.0-68-gxxx-dirty)不是合法包版本——deb 要求
    数字开头、rpm 不允许 '-'。以前只有显式 VERSION=1.0.3 才打得出来;默认路径
    从来没通过过。现在归一化,非数字开头时显式报错。
  · 三处 mktemp -d 落在 /tmp(本机 9.8GB tmpfs),而 staging 要复制 719MB 模型,
    中途 ENOSPC;报错文本指向某个 .onnx 文件,看着像资产坏了。改为落在与构建产物
    同盘的 build/.stage-tmp。
  · 开工前删掉旧的 SHA256SUMS:失败时脚本直接退出、不重算,留着像在为残缺产物背书。
- setup.sh:把包内 /usr/lib/homeagent/models/chinese-clip-vit-b16-onnx 软链到
  <dataDir>/models/…(不复制 754MB、保持 dataDir 可迁移、已有自定义目录不覆盖)
- homeagent.service:ExecStart 改 /usr/bin/homed(deb 装在那里,此前写 /usr/local/bin,
  装了也不会被 unit 用上)、加 ONNXRUNTIME_DIR 与 StateDirectory、MemoryMax 2G→8G
  (实测常驻约 4.5GB,2G 会在首次全量建索引时被 cgroup OOM)
- control-{full,server}:补 libstdc++6 / libgcc-s1(libonnxruntime.so 需要)
- providers/{chineseclip,qwen3vl}:findOnnxLib 支持 ONNXRUNTIME_DIR / ONNX_ML_DIR
  与包内 /usr/lib/homeagent/onnxruntime,随包的运行库才真的会被用上
- postinst:修掉两个让「装完即用」失效的点——它检查 /lib/systemd/system/ 下的 unit
  而 deb 装到 /etc/systemd/system/,于是 daemon-reload/enable **从未执行**;以及
  setup.sh 的失败被 `|| true` 吞掉(正是 initconfig 静默缺陷被藏住的原因)。现在
  三个候选路径都查、失败可见并给出补救命令、首装 start / 升级 restart。
- docs/zh/multimodal-space.md:新增「随包分发」一节,并修正播种判据的说明

验收(从真实 deb 走一遍,不是读脚本):
- 包内 initconfig 已是动态链接,凭据真的写进 config.db
- 解包 → 按 postinst 顺序跑 setup.sh → 包内 homed 冷启动:
  multimodal space active: provider=chineseclip dim=512 fp=cd2a495cf990
  modalities=[text image]
- 全新安装的默认值确实被播种(core.plugin.dir / provider / model_dir 都在)
- 真实对话拿到回复(3.4s,回复中含唯一标记)
- 包内模型 SHA256SUMS 5/5 通过;包内 ORT 与源同 sha256;server 包 722MB
  (旧版 17MB,差额即模型与运行库);full 包同样含全部资产;client 包不含
2026-09-12 08:10:44 +08:00
5299e18cd8 fix(config): 全新安装的默认值播种判据改为显式标记
播种判据曾经是「config 表为空」。而发行包的 postinst 先跑 setup.sh →
initconfig,后者会写一行 webui.listen_addr,于是**全新安装**被判定为
"已有配置"并整体跳过播种:没有 core.plugin.dir(装完 0 个插件)、没有
core.memory.* 路径、也没有随包模型对应的多模态 provider——754MB 产物与
24MB 运行库全成死重量。同样的机制此前已在协议 2 迁移演练里被观察到。

也不能改成"每次都补缺键":老安装升级时被注进新默认值,会让它突然去加载
一个 1.8GB 的模型,那是刻意要避免的静默变重。

故改为显式标记 core.internal.seed_version:
  有标记                        → 已播种,返回
  无标记但有 core.daemon.data_dir → 老安装,只补标记、不播种
  两者都没有                    → 全新安装,播种并打标记

测试:TestSeedDefaultsAfterInitconfigPrepopulate(精确复现 initconfig 的
那一行写入)、TestSeedDefaultsDoesNotInjectIntoLegacyInstall。
2026-09-12 08:10:34 +08:00
6a3439a49c fix(initconfig): 必须带 cgo 构建,且失败不再静默
cmd/initconfig 通过 database/sql 使用 mattn/go-sqlite3,而 build.sh 一直用
CGO_ENABLED=0 构建它:该库在非 cgo 下退化成 static_mock.go 里的桩,sql.Open
是惰性的所以不报错、第一次 Exec 才失败,而 main.go 丢掉了所有返回值。合起来
是一个完全静默的空操作——打印凭据、退出码 0、config.db 里一个字节都没写。
安装脚本把这份凭据写进 credentials.txt,用户照着登录必然失败,全程无报错。

- build.sh: initconfig 改 CGO_ENABLED=1,并写明为何不能图省事去掉 cgo
- main.go: 每个 Exec 都检查;写完**回读比对**(不看返回码,看真实落盘内容),
  不一致即非零退出

反向验证:仍用 CGO_ENABLED=0 构建时,现在 stderr 报
"Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work"
且 exit 1(此前是 exit 0 并把凭据照打印出来)。
2026-09-12 08:10:26 +08:00
ba0b5a1fe0 feat(release): 发行版默认启用本地向量空间(onnxruntime 标签 + chineseclip 默认)
用户要求:后续发行版默认带 ONNX 模型能力。这条要求落到两处,并顺带修掉一个
被它**暴露出来**的真缺陷。

## 1. 构建默认带 onnxruntime(deploy/packaging/build.sh)

`HOMED_TAGS` 默认 `onnxruntime`,需要极简构建时显式 `HOMED_TAGS=` 关闭。
不带标签时 provider 仍注册、但打开即报「requires build tag」并优雅降级——
不静默假装成功。运行期还需要 `libonnxruntime.so`(provider 按
/opt/onnxruntime、/usr/local/lib、/usr/lib 顺序查找),缺失时同样是
「日志里明确错误 + 降级」。

## 2. 新装默认选 chineseclip(internal/config/registry.go)

`SeedDefaults` 写入:
  core.memory.multimodal_space.provider = chineseclip
  core.memory.multimodal_space.options.model_dir = <dataDir>/models/chinese-clip-vit-b16-onnx

选它而不是 qwen3vl:后者实测常驻 9.4GB,多数机器装不下;chineseclip 是
1.99GB(实测,见下)。同时更新两个 ConfigDef 的默认值与描述(WebUI 显示用)。

**老安装不会自动拿到这两个默认值**,这是有意的:`seedDBValues` 对非空配置库
直接返回,`GetString` 缺键时回落到调用方默认值(main.go 传的是空串)。
升级就静默加载 ~1.8GB 模型不是无副作用的事,应由部署显式开启。已写进文档。

## 3. 修掉 ORT 环境被重复初始化 + 误销毁(internal/nlp/onnx.go)

这是「默认带标签」才暴露的缺陷:此前不带标签时进程内不会有多个 ORT 消费者。

- `NewONNXParser` 无条件 `InitializeEnvironment()` → 若多模态 provider 先初始化,
  这里报「The onnxruntime has already been initialized」并**降级**(实测日志:
  `ONNX parser init: init onnx env: ... using fallback`)。
- 更严重的是失败路径与 `Close()` 里的 `DestroyEnvironment()`:它会把别人
  (多模态 provider)正在用的进程级环境一起拆掉,让对方的会话失效。

改为:初始化前先 `IsInitialized()`;**任何消费者都不销毁环境**(随进程存活),
只销毁自己的会话。providers/chineseclip 与 providers/qwen3vl 本来就是这个约定,
现在三处一致。

## 验证(实测)

- 全新数据目录启动:配置库出现上述两个默认值。
- 模型未安装:`multimodal space active` 不出现,代之以明确错误
  (点名缺失的 embed_config.json 路径 + 已注册 provider 列表)+ 降级,不静默。
- 模型就位:`multimodal space active: provider=chineseclip dim=512 fp=cd2a495cf990
  modalities=[text image]`。
- 内存:同一份 homed,启用时 RSS **1.99GB**(峰值 2.09GB),不启用 **0.17GB**。
- NLP 修复:日志由 `ONNX parser init: ... using fallback` 变为 `dep parser initialized`。
- 构建矩阵:`go build/vet ./...` 与 `-tags onnxruntime` 两种都过;
  `bash -n deploy/packaging/build.sh` 通过。

## 未做(明确记录)

- `libonnxruntime.so`(24MB)与 Chinese-CLIP 产物(754MB)目前都需自行安装/导出,
  发行版尚未打包它们。若要让「默认启用」在干净机器上真正开箱可用,需要决定
  是随包分发、安装时下载、还是保持文档指引。
2026-09-12 00:10:32 +08:00
bfdb395731 feat(memory): 新增 chineseclip provider —— text+image 的小体积可商用向量空间
## 为什么

用户决定「本轮不覆盖 video,先支持 text+image」。这一刀正好解锁了此前
「小 + 可商用 + 覆盖视频」三者不可兼得的僵局:不要求视频后,唯一同时满足
**小、可商用、中文原生** 的选项是 Chinese-CLIP ViT-B/16。

实测对比(同机、真实跑出来的数字):

| | Chinese-CLIP | jina-v5-omni-nano | Qwen3-VL-Emb-2B |
|---|---|---|---|
| 参数量 | 188M | 1.04B | 2B |
| 产物 / 常驻内存 | 754MB / **1.15GB** | ~2GB / 2.23GB | 8GB / 9.4GB |
| 维度 | 512 | 768 | 2048 |
| 许可 | **Apache-2.0** | CC BY-NC(不可商用) | Apache-2.0 |
| 视频 | 无 | 有 | 有 |

本机可用内存只有 5.3GB,Qwen 的 9.4GB 无法进程内使用;而 ORT format + mmap
那条路被证实当前不通(转换器对三段图段错误;走通还需同时升 ORT 运行时与
Go 绑定,v1.36 要求 API 29 而本机只有 28)。1.15GB 则可以直接进程内跑。

**代价已写进包注释与文档**:CLIP 是双塔对比学习,text↔image 是强项,但纯文本
语义明显弱于 MLLM 型嵌入器;文本检索仍由既有词向量/TF-IDF 路径兜底。
需要更强文本语义或视频时切回 qwen3vl。

## 内容

- `providers/chineseclip/`:按公共 SPI 实现的 provider(注册名 `chineseclip`),
  含 BERT WordPiece 分词器、图像预处理、ONNX 双塔推理、无标签 stub。
- `scripts/export_chineseclip_onnx.py`:从官方权重导出规范产物 + 冻结参考,
  自带逐用例 PyTorch 对比与覆盖度断言(计划集合≠执行集合即非零退出)。
- `cmd/homed/main.go`:空白导入两个 provider,由配置选其一。
- `go.mod`:`golang.org/x/text` 由间接依赖转为直接依赖(删音标需要 NFD)。

## 实现要点

- **分词器逐 token 对齐官方**。第一版探针自己拼 BertTokenizer(只给 vocab.txt、
  没删音标、中文没逐字切),中文被整体切成 [UNK],三个不同句子产出几乎相同的
  向量(余弦 0.98)——差点把「模型坏了」当成结论。官方配置是 do_lower_case=true
  + 删音标生效 + 中文逐字切分;`TestTokenizerMatchesOfficialReference` 钉住
  逐 token 一致。
- **图像缩放自写 bicubic**(复刻 PIL 的 precompute_coeffs + a=-0.5 核),不引
  golang.org/x/image:它未进本机模块缓存,且最新版要求把整个工具链升到 Go 1.26,
  为一个缩放函数动工具链不划算。
- **归一化在 provider 侧**(两个塔的图里都没归一化),检索按余弦。
- **指纹覆盖全部影响语义的产物**:两个 ONNX 图 + vocab.txt + embed_config.json,
  读不到就写 MISSING(跳过等于对缺件不敏感)。
- 会话 Run 用 runMu 串行化(ORT 会话不保证并发安全),创建/销毁用 mu。

## 模态范围

只声明 `text` 与 `image`;`audio`/`video` 明确返回 `ErrUnsupportedModality`,
绝不用别的模型向量冒充(这是「音频明确 unsupported」纪律的落地)。

## 验证(实测)

导出侧:10 个用例(5 文本 + 5 图像)ONNX vs 官方 PyTorch 全部
`cos = 1.000000000`,覆盖度断言 10/10 通过。

Go 侧(`CHINESECLIP_MODEL_DIR=... go test -tags onnxruntime ./providers/chineseclip/ -v`):
11/11 通过,其中
- 文本 5 用例 `cos = 1.000000000000`(逐位一致)
- 图像 4 纯色用例 `cos = 1.000000`(与官方预处理在 6 位小数内一致)
- 跨模态判别:红图对「红色」文本高于「蓝色」文本
- 模态拒绝 / 空输入 / 指纹稳定 / 产物缺失报错

顺带修掉测试自身的一个假通过:参考向量是**未归一化**的原始输出(模长 10~36),
原先「点积当余弦 + 单侧下界」会让 13.6 也判过,已改为真余弦 + 双侧容差。

构建矩阵:`go build/vet ./...` 与 `-tags onnxruntime` 两种都过;
`providers/... pkg/... internal/config/... internal/memory/vector/...` 回归通过
(qwen3vl 的 TestVideoModelInputMRope 需要 QWEN_ONNX_MODEL_DIR 指向含视频档的
v3 目录,缺该环境变量时用的是只有文本+图像的目录,与本改动无关)。

## 未做(明确记录)

- 发行版默认 provider 与构建标签变更:留下一提交(涉及打包与模型分发策略)。
- 模型产物(754MB)不进仓库,由导出脚本生成。
2026-09-11 23:58:53 +08:00
d1959cbe80 feat(core): 注入行为的记忆/裁剪标志位落地 + jieba 词库内嵌 + Windows 改走 WSL
配套 SDK 提交:homeagent-sdk ba49dfd(公开 API 纯追加,无签名变更)。
本仓第三方的库镜像同步至该版本,以保证全新 clone 能编译。

## 1. 注入标志位(内核侧)

- 7 条注入路径(排队/中断/同步 × 纯文本/带媒体 + 旧 NoMem 变体)解析并转发
  no_memory / context_policy / cleaner_name;策略在入口**校验**,
  非法值报错而不是静默降级成 none(降级会让调用方以为自己声明的裁剪在生效)。
- 新增 validateContextPolicy(与 tool.register 同一套规则)与 pubSdkInjectOpts。
- input.register 不再手写字段白名单重建 ChannelDef,改为整体传递 + 补 ContextPolicy。
- io 层:applyInjectOpts 把标志位写进事件 payload,仅非零时写
  (零值与旧 payload 逐字节一致,事件订阅方与旧内核都不受影响)。
- ioAdapter / procCore / internal-sdk 别名补齐六个 *Opts 实现。

## 2. 修掉「输入无条件裁剪」这个真缺陷

eventloop 此前对**每条非中断输入**都调 `context.Prune(...)`:破坏性(低相关事件被
归档移出上下文)且无法从调用点看出是谁触发的。改为 pruneOnInput/pruneDeclared:

  优先级:注入点声明(payload.context_policy)> 通道声明(ChannelDef.ContextPolicy)
          > 默认**不裁剪**

查询向量仍取清洗后的内容;新增 cleanInputFor 解析清洗文本,优先级为
注入点声明的 cleaner(cleaner_name)> 按 source 查到的通道 cleaner > 原文,
名字查不到时**记日志再回退**(注入是 fire-and-forget,插件看不到错误,
至少要在内核日志留下「你声明的清洗没生效」的痕迹)。

## 3. jieba 词库内嵌(修「猜 GOMODCACHE → 静默失效」)

原 jiebaDictDir() 去猜 GOMODCACHE/GOPATH/~/go/pkg/mod,部署机上通常没有 Go 模块
缓存 → GetJieba() 返回 nil → 分词/关键词提取/NLP 依存解析(进而 doc→graph 三元组
抽取)/静态词向量 tokenizer **一律静默返回空列表**,只有一行日志。本机看起来正常
只因开发机与生产机重合、恰好有那份缓存。

现在词库随二进制分发:internal/memory/jiebadict/ 5 文件约 11.6MB + go:embed,
按**内容哈希**命名缓存目录落盘(词库升级不复用旧文件),已齐全则跳过写入。
模块缓存降为兜底。homed 体积 32MB。

顺带确认(并有测试佐证):gojieba 的 Tag() 不需要 pos_dict/ 目录——
cppjieba 的 PosTagger 从主词典每行的词性列取 tag。

## 4. homed 放弃 Windows 原生,改走 WSL2

插件体系依赖「继承的 fd」+「统一共享内存区的段内偏移解引用」,Windows 既无 fd
继承语义,其句柄模型也无法表达后者;强行适配等于再维护一套平台专属 ABI
(C ABI 时代三套 ABI 并存曾导致改写型插件在某平台静默失效)。

- cmd/homed/platform_{windows,other}.go:原生 Windows 启动即拒绝并打印 WSL2 指引。
- internal/plugin/proc/shmalloc_windows.go:allocShm 直接返回「请用 WSL2」,
  **不返回半可用的段**(与 shmalloc_other.go 同风格:未支持平台显式报错);
  procEnvForShm 返回 nil。顺手修掉两处长期编译错误
  (cryptorand→rand、h.evData→h.unified.evtData),使 GOOS=windows 至少能编译。
  注:homed 本就编不出 Windows——internal/memory 依赖 cgo-only 的 gojieba。
- deploy/packaging/installer.nsi:不再安装 homed.exe/initconfig.exe,改为携带
  **linux payload** 并调用新的 install-via-wsl.ps1;退出码 20/21 表示
  「需先装 WSL/发行版」,走指引而非报错。
- deploy/packaging/windows/install-via-wsl.ps1(新):检测 WSL → 引导安装 →
  确保 WSL2 → 送包进发行版 → 在 WSL 内按 Linux 方式安装。**复用 Linux 包与
  linux/setup.sh**,不另写一套安装逻辑;落点与 deb 布局统一
  (/usr/bin/homed + /usr/lib/homeagent/setup.sh)。
- deploy/packaging/linux/setup.sh:API Key 允许 HOMEAGENT_API_KEY 覆盖
  (否则安装器界面显示一份、config.db 里另一份 → 登录不上)。
- deploy/packaging/build.sh:windows 目标只构建 waiter + gui,并新增
  stage_linux_payload 把 Linux 包暂存给安装器;homed/initconfig 在 windows
  目标下明确拒绝。

## 5. 插件调用点统一写明意图

- webui 的 OpenAI 兼容端点(固定提示词模板)→ InjectTextSyncNoMemory。
- agentcli 的 5 处纯状态通知(已启动/超时/执行结束/进程退出/读取结束)→ NoMemory;
  **带输出**的 2 处(定时反馈、有新输出)刻意保留记忆并注明理由。
- timer 的定时提醒 → NoMemory(中断本来也隐含 NoMemory,这里是写明意图)。

## 6. 版本

meta.Version 仍为 1.2.0(main 是下一个未发布中版本);
SDKCompatibleVersion 1.1.0 → **1.2.0**(本内核已实现 SDK 1.2.0 全部新增方法)。

## 测试

- core:默认不裁剪(无声明/none/空)、通道 opt-in、注入点双向覆盖通道、
  nil context/io 安全、cleaner 优先级与未知名回退。
- io:零值 opts 与历史 payload 逐键相同;text/中断/媒体三类注入标志位都落到
  payload;旧方法仍生效。
- proc:validateContextPolicy 只接受 ""/none/prune,报错含位置与实际值;
  **跨进程** e2e——testdata 插件经 io.injectText 送出三个标志位,断言它们穿过 RPC
  到达内核。
- memory:模块缓存不可见时内嵌词库仍可用(分词与 POS 内容词均非空)、
  落盘幂等、内容哈希稳定。

验证:go build ./... / go vet ./... / go vet -tags onnxruntime ./...
      go test -short ./internal/memory/... ./internal/nlp/... ./internal/plugin/...
      ./internal/agent/{core,io}/... ./pkg/...
2026-09-11 20:31:50 +08:00
a37bc7333e refactor(memory): 核心不再适配具体模型——公共 embedding provider SPI + 注册表
问题:cmd/homed 里 `case "onnx": qwen.New(modelDir)` 把模型适配写进了核心,
`type=onnx` 名义上是格式、实际写死了一个模型家族;2117 行 Qwen 专属代码
(BPE、chat template、M-RoPE、Vision_gN 命名)住在内核树里,还带着一对
`//go:build onnxruntime` 的 stub。加任何新模型都要改内核。

现在核心只认一个模型无关的公共契约(pkg/embedding):
- 输入是不透明的 Data+MIME,解码/预处理/时序分组全归 provider
- 能力是数据(Info.Modalities),不是接口方法——新增模态无需改核心接口
- 不支持的模态返回 embedding.ErrUnsupportedModality(可 errors.Is 识别)
- 按名字注册,重复注册 panic;Options 是 provider 私有命名空间,核心不解释

改动:
- 新增 pkg/embedding:Modality/Purpose/Input/Info/Provider/Config + 注册表
  (Open 校验 Info,ValidateVector 在入库前拦下维度错与非有限值)
- providers/qwen3vl:Qwen 实现整体移出内核(git mv),实现公共 SPI 并自注册
- internal/memory/vector:新增 ProviderAdapter(公共 SPI → 内部小接口);
  ErrModalityUnsupported 改为公共哨兵别名;删除 VideoEmbedder 可选接口
  (那正是「核心为每个新模态长方法」的坏味道)
- http embedder 也变成普通 provider(注册名 http)
- cmd/homed:删除 qwen import 与 onnx/http 分支,改为按 provider 名打开 +
  透传 options.*;provider 打开失败只警告并禁用多模态检索,不影响启动
- config:multimodal_space.type/onnx./http.* → provider + options.*
- 删除 internal/memory/qwen(整体搬迁)

测试:
- pkg/embedding:注册表隔离/未知名字/非法 Info 自动关闭/ValidateVector
- vector:适配器原样透传字节与 MIME、维度错被拦、Close 幂等且停止使用、
  两个哨兵 errors.Is 互通
- providers/qwen3vl:新增公共 SPI 全链路集成测试(Open→Info→Embed→
  未知模态哨兵),并明确断言 Info 不声明 video

已知未完成(不得当作已验证):
- 视频冻结回归 TestEmbedderVideoMatchesONNXReference **显式跳过**:Go 侧
  video 模板缺少 processor 按时间组插入的字面时间戳文本
  (<0.0 seconds>/<1.0 seconds>),同一输入 Python seq=1190(1152+38)、
  Go 只有 22 个文本 token。时间戳也占 M-RoPE 位置,故现有 M-RoPE 自洽断言
  通过不能证明与官方实现一致。修复属 provider 内部工作。
- 视觉侧三档已导出并逐档校验通过(cos 1.000000119/1.000000119/1.000000000)

验证:go build ./... ;go vet -tags onnxruntime ./... ;
go test -short ./internal/memory/... ./internal/agent/core/... ./internal/sdk/... ./pkg/...
;onnxruntime 下 providers/qwen3vl 全绿(视频为显式 skip)
2026-09-11 18:26:19 +08:00
1a02971f88 feat(memory): 千问三段式 ONNX 嵌入补齐——可复现导出脚本 + Go 侧首次完整验证
此前三段式拆分后 ONNX 路径从未从 Go 侧跑通:embedder_onnx_test.go 仍引用
分段前的 API(e.renderInput、TextTower.onnx、旧目录),go vet -tags onnxruntime
直接编译失败。导出脚本只在 /tmp 且硬编码本机路径、从第三个目录拷贝固定形状的
Vision.onnx,完全不可复现。音频会被视觉塔编码,静默往统一空间灌入错误坐标。

本提交补齐这些缺口:

一、可复现导出脚本(scripts/export_qwen3vl_embedding_onnx.py)
- 自动拉取模型(HuggingFace 优先,失败回落 ModelScope,支持 HF_ENDPOINT 镜像);
- 导出 TokenEmbedding + Transformer + Vision 三段图,图文共用同一 token
  embedding、28 层 Transformer、last-token 池化与 fingerprint;
- 双重自检(不可省):分段 PyTorch vs 完整模型 + 导出后的 ONNX vs 完整模型,
  cos < 0.999999 即非零退出——「能加载」不等于「算得对」;
- 默认把 L2 归一化后的冻结参考向量写入产物目录(qwen_reference.json)——
  Go 测试据此做逐维冻结回归,且「该目录是哪次导出的」从文件本身可追溯;
- --verify-only 校验既有产物不重新导出,可用来确认线上在用的图没坏。

关键实测结论(已写入 docs/zh/multimodal-space.md 与长期记忆):
原生多帧视频不可行——Qwen3-VL 视觉塔把 grid_thw 当 Python 值消费
(grid_thw.tolist()),legacy tracer 固化为常量,导出后图中根本没有 grid_thw
输入,换帧数调用直接 Invalid input name: grid_thw。故视觉塔固定 (1,48,48),
视频由上层抽帧后逐帧按图像编码(同模型/同维度/同 fingerprint),音频明确
unsupported。

二、模态边界(vector.ErrModalityUnsupported)
- 新增 vector.ErrModalityUnsupported:表示「该模态不在本统一空间的原生覆盖
  范围内」,与普通错误语义不同——调用方应把它当「永远不会有向量」而非
  「本次失败、下次重试」;
- qwen.EmbedImageDense 按 mime 拒绝 audio/* 与 video/*:此前它会拿视觉塔
  去解音频字节,往统一空间灌入语义错误的坐标且静默;
- reembedStaleMedia 对 ErrModalityUnsupported 不计失败、不重试、不用别的
  模型向量顶替(TestReembedStaleMedia_SkipsUnsupportedWithoutFaking 守住)。

三、Go ONNX 测试首次完整通过
- 重写 embedder_onnx_test.go:修复编译 + 文本冻结回归 + 图像冻结回归 +
  两条阴性对照(不同输入必须不同、图像与文本必须不同)+ 不支持模态断言;
- 参考值从产物目录的 qwen_reference.json 读取(不在测试里硬编码浮点);
- 用线上部署产物实测全部通过(text cos=0.999999940, image cos=0.999999762)。

四、.gitignore 修复
- /scripts/ 此前被列在「运行时产物」下,但它是作者维护的工具目录
  (模型导出、侧车、部署校验),deploy/systemd/embed-sidecar.service 直接
  引用 scripts/embed_sidecar.py,忽略它会让那份 unit 在别人的机器上指向
  不存在的文件。改为只忽略 __pycache__。

五、文档(docs/zh/multimodal-space.md)
- 获取/启用/产物契约/模态边界/验证/资源成本/与现有部署产物的等价性。

验证:go build ./...、go vet ./...、go vet -tags onnxruntime ./...、
go test -short 全部通过;ONNX 标签测试对线上部署产物全部通过。
2026-09-11 13:45:25 +08:00
5836c2ce5c refactor(memory): 拆除描述式媒体索引,媒体成为一等块并按原生向量融合
背景:此前媒体是靠「生成的描述文本」将就进记忆的——写 marker 进正文、
再由正则反解成 media_refs 与图库里的 type=Media 实体。这条链路有三个
致命缺陷:描述由异步模型生成(未生成前媒体等于不存在)、语义检索实质上
只搜描述文字、图库里的「媒体节点」是描述文本的投影而不是媒体本身。

本提交把这条链路整体拆除,媒体改为按自己的原生向量参与记忆:

一、描述链彻底删除(无残留、无兼容分支)
- media.Item 去掉 Description/DescribedBy 与对应列;
- 删除 Store.Describe / Store.Search / Store.Pending;
- 删除 Agent.mediaDescribeLoop / describePendingMedia 与配置项
  core.memory.media.describe_on_ingest;
- SDK 侧 MediaAttachment 去掉 Description(见 SDK 仓独立提交)。

二、marker 机制删除,媒体归属改为结构化块边
- 删除 mediaMarkerLine/parseMediaMarkers/mediaEntityName/mediaTriplesFromText/
  extractMediaDigests/sentenceWithMediaMarkers/docMediaContext;
- memory.Triple 新增 MediaDigests 结构化字段;句子文本保持原样,
  不再被 marker 污染;
- 块以 sentence --contains--> block / document --contains--> block 结构边
  挂到承载节点(新增 documents 表与 document 节点种类);
- 模型未给原句时用「主谓宾。」拼一句自然语言作落点,不造 marker 文本。

三、旧数据迁移(幂等)
- 新增 GraphDB.MigrateLegacyMediaEntities:把 type=Media 的旧实体按短 digest
  还原成原生块、挂回原句子、删除旧实体与描述关系;Agent 启动时执行;
- CleanupOrphanedSentences 同时看关系引用与块边,避免把只靠块存活的句子
  连同块边一起删掉。

四、向量融合:媒体按图本身被召回
- 新增 vector.FuseVectors(逐维求和 + L2 归一化);
- Doc.DenseVec = 文本向量 ⊕ 文档块的媒体向量(同 fingerprint 才融合),
  新增 Doc.DenseFP,指纹变化触发重算;
- ContextEvent.DenseVec 同理融合事件块;事件新增 DenseFP,Prune 只在
  同一统一空间内比稠密余弦;
- 跨模态视觉路只召回「仍被某层记忆块持有」的媒体,CAS 全库字节不再
  直接充当记忆检索结果。

五、同时纳入本分支既有的嵌入基础改造(此前工作区未提交,缺它 HEAD 不可构建)
- internal/tfidf 懒回退包、千问三段式多模态 ONNX 空间的 Go 侧
  (qwen/embedder.go、image.go、model_input.go)、CLIP 移除、
  sdk.NewStore 分词器签名与调用点、embed 侧车 systemd 单元。

验证:go build ./... 、go vet ./...(含 -tags medialive)均通过;
在 HEAD 的独立 worktree 上重放本次暂存集后 go test -short ./internal/...
全部通过(端口冲突类用例在隔离环境中亦通过)。未提交工作区中与本改造
无关的改动(HarmonyOS、waiter、devicebridge、plan.md 等)。
2026-09-11 11:45:24 +08:00
dae01f9c06 refactor(memory): 移除 media_refs/引用计数,媒体成为一等记忆块
媒体此前是"文本块 + digest 引用 + owner 账本 + 独立 GC":ContextEvent.Media
记 digest,media_refs 表用 owner_kind/owner_id 保活,ref_count 决定 GC 能否清。
这与文本记忆块的管理方式不一致,也是本次一并纠正的核心偏差。

改为与文本块完全一致的生命周期:

1. 一等记忆块直接由所在层持有
   - ContextEvent.Blocks / Doc.Blocks / GraphDB memory_blocks
   - 块带 modality/digest/MIME/size/vector/fingerprint,文本、图片、视频同构
   - Context→Document→Graph 迁移的是块本身(ID 不变),迁移后清空源容器,
     同一块不同时存在于两层

2. 删除平行生命周期账本
   - media.Store 去掉 media_refs 表、OwnerKind 常量、RefCount 字段、
     AddRef/DropRef/DropOwner/Refs、ref_count 列与索引
   - 删除 mediaGCLoop、GC(keep,minAge)、容量上限与 media.gc_* / media.max_mb 配置
   - 媒体内容在块被永久删除时一并删除(media.Store.Delete + forgetPayloads),
     与"删除文本块即删除内容"同一语义

3. L3 原生结构
   - memory_blocks / memory_block_edges(contains/depicts/derived_from)
   - 边端点必须是真实图节点,不再用 owner 字符串伪装关系
   - BlocksForNode 支持 sentence --contains--> block 反查

4. SDK 与检索同步
   - 插件附件/标记直接变成块,不再 AddRef
   - 跨模态检索改用 QueryMediaScored(CAS 内不再有孤儿缓存需要过滤)

测试全部改写为块语义:删除 refcount/media_refs/GC 断言,新增块迁移、
单层不变量、Delete 语义与并发删除回归。

注:cmd/homed/main.go 同时携带工作区中既有的 CLIP→Qwen 模型目录接线改动。
2026-09-11 10:57:22 +08:00
e44164f5bd feat(memory): Graph 原生一等记忆块节点与结构边(§13.12)
媒体/文本在 L3 不再是正文标记反解出的代理实体,而是带原生
modality/digest/MIME/size/vector/fingerprint 的 memory_blocks 节点;
contains/depicts/derived_from 等语义边落在 memory_block_edges,
端点必须是真实图节点(block/entity/sentence),不复用 owner 字符串。

本步只建立存储与查询能力,不接入 media_refs,也不改变 L0/L2 路径。
2026-09-11 10:08:59 +08:00
96c1d7baae fix(memory): 千问文本塔补齐 tokenizer post_processor 与真实 ONNX 回归
验证 Go 端到端路径时发现:HuggingFace 的 tokenizer.json 带 TemplateProcessing
post_processor,规则是 `$A <|endoftext|>`——即每段输入末尾都会追加一个
`<|endoftext|>`(151643)。它正是图内 last-token 池化的锚点:

- 漏掉它:ONNX 仍能运行(不会报错),但池化取到的是模板末尾的 assistant
  起始符,而非 post token,整条嵌入向量与上游不一致;
- 截断语义:HuggingFace 在 truncation=true 时先把正文截到 maxLen-1,
  再保留末尾 post token(实测 600×"记忆"→ [511 正文][151643])。

改动:
1. Tokenizer 新增 encodeModelInput(text, maxLen):Encode 后追加 post token,
   并在超长时先截到 maxLen-1;缺 <|endoftext|> 直接报错(防静默错误)。
2. Embedder.VectorizeDense 改用 encodeModelInput。
3. 测试:
   - TestEncodeModelInputPostProcessor:短文本+post token;长文本按 512 截断
     且尾 token 为 post token(用 600×"记忆"确保真的触发截断分支)。
   - embedder_onnx_test.go(onnxruntime 标签):用 Python onnxruntime 1.28
     生成的冻结参考向量验证完整 Go 路径(模板渲染→BPE→ONNX→L2 normalize),
     逐维 diff ≤ 2e-5;同时断言模型输入恰好 23 token 且末尾是 post token。
     产物不在时跳过(与 tokenizer_test 相同约定)。
   - 修正先前测试误用 400×"记忆":BPE 把"记忆"合并为单 token,400 次只有
     400 token 不触发截断;改用 600 次后确实走到 maxLen-1 分支。

其余(ORT 库路径、指纹纳入 .onnx.data)一并随本提交带上。

验证:go test ./internal/memory/qwen 与 -tags onnxruntime 全绿;
FP32 图与 PyTorch 在短/长/等长批次/真实 padding 批次上余弦均 ≥0.99999994。
2026-09-11 03:09:01 +08:00
8e88ae789f feat(memory): 千问文本塔 ONNX 嵌入器(onnxruntime 标签,含 stub)
与 internal/memory/clip 同模式:`//go:build onnxruntime` 编真实实现,无标签时
走 stub,默认构建不链接 onnxruntime、行为不变。

加载契约(目录由 core.memory.multimodal_space.model_dir 指定):
TextTower.onnx + 外部权重分片、tokenizer.json、embed_config.json。
图内已含 last-token 池化,输出即 [batch, dim];L2 归一化在 Go 侧做。

两个刻意的设计选择:

1. **EmbedImageDense 明确报错,不返回零向量**
   导出的是文本塔,视觉塔未导出。返回零向量会让「写入了但检索不到」,
   把跨模态检索失效变成静默故障;明确报错则调用方(mediaref.go)log 后
   跳过写向量,文本路径不受影响。

2. **Fingerprint 只哈希图文件 + 配置 + 外部权重的文件名与大小**
   该目录有 6.5GB 权重分片,启动时全读一遍要几十秒、会阻塞 homeagent 启动。
   换模型必然改变文件集合或大小,足以识别切换;代价是理论上存在
   「大小相同但内容不同」的漏判,对本地单机部署可接受。已写入注释。

模板渲染(renderInstructionInput)放在无构建标签的 tokenizer.go,因此可被
测试覆盖:参考数据里有该模板串的用例,逐 token 对齐验证——模板差一个字符,
池化取到的「最后一个有效 token」位置就变,向量就不同,且不会报错。

验证:6 个测试全绿;go build ./...(stub)与 go build -tags onnxruntime
(真实实现)均通过。
2026-09-11 00:16:45 +08:00
e985151da9 feat(memory): 千问字节级 BPE 分词器 + 与上游逐条对齐的回归测试
为「千问嵌入模型导出 ONNX 并内嵌」的 Go 侧准备。CLIP 那套 tokenizer 不能复用:
CLIP 是「小写化 + 空白规整 + 词表 BPE」,千问是 **GPT-2 式字节级 BPE**
(先按字节映射到安全 unicode,再对映射结果做合并),中文与空白输入的切分
完全不同。

实现中撞到两个与上游对齐的坑,都由测试暴露:

1. **`\s+(?!\S)` 的语义依赖正则回溯**,不是「等价于 `\s+`」。
   `\s+` 先贪婪吃完整段空白,发现后面是非空白导致 `(?!\S)` 失败,于是回退
   一个字符,**正好留下末尾一个空白**给前面以 ` ?` / `[^…]?` 开头的分支合并。
   这直接决定切分点:`"   leading"` 会切成 `"  "` + `" leading"`,
   而不是 `"   "` + `"leading"`。RE2 不支持 lookaround,近似改写必然对不上,
   所以改成按分支顺序**显式实现**(含 `\s*[\r\n]+` 的回溯语义)。
   实测:近似改写时 26 条里错 3 条,全部是空白串用例。

2. **Go 的 `\s` 只有 ASCII,且 regexp 不支持二进制属性 `\p{White_Space}`**
   (只支持 script/category,直接写会报 invalid character class range)。
   而上游 Rust regex 的 `\s` 正是 White_Space。改用 `unicode.IsSpace` 作为
   唯一判据,避免全角空格/NBSP/行分隔符的切分点漂移。

另外特殊 token(`<|im_start|>` 等 24 个 AddedToken)必须**整体优先匹配**并
按长度降序,否则会被 BPE 拆成子 token,模型收到的输入就变了——且不会报任何错。

验证:`testdata/qwen_tokenizer_reference.json` 由 HuggingFace 真实 tokenizer
生成(26 条用例,覆盖中/英/中英混排/数字/各类空白形态/标点/emoji/特殊 token/
长文本/空串/单字符边界),Go 实现逐条精确对齐。字节↔unicode 映射另测双射性
(有碰撞会让不同字节编成同一 token,静默产生错误输入)。
2026-09-11 00:11:58 +08:00
b393b7072c fix(ctx): prune 的查询向量改用插件 Cleaner 清洗后的有效内容(§13.8)
ContextPolicy=prune 上线时直接把**原始**工具结果传给 RelevanceContext.Prune,
而 Prune 的入参是**相关性查询向量**——它决定保留/归档哪些上下文事件。于是
ANSI 转义、base64、JSON 包装等噪声全被编进查询向量,打分失真,裁掉本该
保留的事件。

而 ToolDef.Cleaner 的契约本就写着「仅在向量化/jieba/蒸馏时调用」,裁剪正是
在向量化——所以这是**回归契约**,不是新增能力。此前只在构建事件向量
(context.go 的 toolOutputClean)时用了 Cleaner,裁剪查询这一处漏了。

回退规则(Cleaner 是计算层优化,不能因它失效而丢内容):
- 未注册 Cleaner → 原文
- RPC 失败 → 原文(proc 侧 cleanerProxy 已有此保证)
- 返回空串 → 原文(空串会让查询向量退化成零向量,所有事件相关性相同,
  等于随机裁剪)

验证:TestToolOutputForQueryAppliesCleaner(Cleaner 被调用恰好一次且用其
结果;无 Cleaner / nil stageHost 回退原文)、
TestToolOutputForQueryEmptyCleanFallsBack。

顺带把 §13.13 第 5 条(反向大结果)按核实结论结掉为「不做」:核实发现根本
不存在 llm.chat(llm.* 只映射切换 LLM 源),唯一可能返回大结果的 doc.query
没有任何外部插件使用且已被 CapDocMemory 能力门限制。留成永久 TODO 只会误导。
2026-09-10 23:59:12 +08:00
50ba4a64cb feat(shm): 文档/知识正文入共享内存 + 协议版本 bump 到 2(§13.13)
## 数据面补齐

- doc.insert / doc.insertWithMedia:新增 doc_ref / attachments_ref,
  模板序列化后 putValueInArena
- knowledge.add:新增 content_ref(内容是 JSON 字符串,读出后再解一层)
- 抽出通用 resolveJSONRef(resolveBlocks 也改用它),三处共用一套
  “共享优先、内联回退”逻辑

## 协议版本 bump:让错配显式失败,而不是静默坏

这是本轮更重要的部分。§13.6/§13.13 改了内核→插件 payload 的承载方式,
两种错配都不会报错、只会静默失效:

- v1 插件只读内联 args(tool/cleaner/output)→ 遇到 v2 内核拿到空参数
- v2 插件发 blocks_ref → v1 内核反序列化时静默忽略(旧内核
  io.setToolBlocks 还是桩实现)

现场表现为“输出变空 / 图注入没反应”,极难定位。所以把 ProtocolVersion
与模板 procProtocolVersion 一起 bump 到 2:双方都是等值校验,v1 插件遇上
v2 内核会在建链时明确报“协议版本不匹配…请用配套 plugindev 重编”。

测试里把“错误必须带出重编指令”也断言上了——生产上碰到它的现场就是
“只更新了内核没重编插件”,光报“不匹配”定位不到行动。

testdata 8 个插件的 protocol 同步更新(badprotoplugin 仍用 999 验证拒绝)。
工具链已重建并安装(协议 2,内嵌 blocks_ref/doc_ref/content_ref),
回滚副本 plugindev.bak-20260910-232544。

验证:-race 全绿。新增 KnowledgeAddViaArena(12000B 正文)、
KnowledgeAddInline、DocInsertViaArena、SetToolBlocks 三例 +
真实模板 e2e + 协议不匹配断言强化。

§13.13 第 5 条(反向大结果)范围更大——需把内核→插件的应答路径整体改成
“大结果写段 + 返回 ref”,涉及 callCore 的返回处理与 doc.query/llm.chat 等
所有读大结果的 method。已在 plan.md 标注未做,不冒充完成。
2026-09-10 23:26:16 +08:00
15d912ef34 feat(shm): 媒体块入共享内存 + 实现 setToolBlocks 桩(§13.13)
两个问题叠在一起,先发现的是第二个:

1. **io.setToolBlocks 在内核侧是桩实现**——直接返回“多模态注入待共享段
   二进制通道落地”。也就是**子进程插件调 SetToolBlocks 必然失败**(模板
   只 log 一行),只有内置插件(multimodal)能用。之前还有测试把这个错误
   当作预期行为断言着。
2. 媒体块(injectMedia 系列 + setToolBlocks)把 blocks 内联在 RPC JSON 里,
   而本地生成的图/音频是 base64 data URL,一张图可达数 MB。

顺带修掉一条与设计相悖的旧注释:injectMediaParams 写着“data URL 已是
base64 文本、再套一层二进制不会更小,所以走 JSON”。那只算了体积,漏了
两件更重要的事——内联时整份 base64 要在 RPC 报文里再编码/再拷贝一遍;
以及内容本体不在共享段里,插件回调就无法就地改写,只能各持一份拷贝。
共享内存的意义是后者。

实现:
- injectMediaParams 加 BlocksRef;新增 resolveBlocks(共享优先、内联回退)
- 真正实现 MethodIOSetToolBlocks(空块明确报错,不静默成功)
- CoreSDK 接口加 SetToolBlocks,procCore 转调 internal/sdk
- 模板:SetToolBlocks / injectMedia 三兄弟经 putValueInArena 传 blocks_ref;
  同步调用用 mediaArgsOwned 拿释放函数,**不能在应答返回前释放槽**,
  否则内核读到已释放内存

验证(-race 全绿):
- TestCoreHandler_SetToolBlocksViaArena / Inline / EmptyRejected
- TestE2E_RealTemplateSetToolBlocksViaArena:真实 SDK 模板编译的插件,
  9000 字节 base64 图经 blocks_ref 完整送达

工具链已同步:/usr/local/bin/plugindev 重建为新协议(内嵌 blocks_ref),
回滚副本 plugindev.bak-20260910-224255。注意内核与全部插件必须同批替换
——只换一边会静默坏(新内核+旧插件=输出 payload 变空;新插件+旧内核=
媒体注入静默失效),已记入长期记忆。
2026-09-10 22:45:19 +08:00
2a4315cfe4 docs(plan): 修正 §13.13 判据——按「回调能否就地改写」而非 payload 大小
之前把共享内存的理由写浅了(当成“省管道 / 避免大 payload”)。真实目的是
找回 .so 时代的能力:插件回调(Cleaner / Stage / 工具)与内核同进程时可以
直接就地改写参数与结果;多进程化后若靠 RPC 来回发消息,回调就只能
“读一份、回发一份”,丢失就地改写语义。共享内存是把内容放进段里、回调就地
改、只回描述符。

因此判据应是「插件回调要能就地改写的内容有没有留在段里」,不是 payload 大小。

按新判据核实:回调主线已达成——
- Stage:invokeStage 只发 {Stage, Seq},插件就地改写,应答只回 DirtyFields 计数
- Cleaner:发 {Scope,Name,Frame,InputLen},回 TextRef(16B 描述符),内容不随 RPC 走
- tool.invoke:内核标定 Frame,结果 ResultRef,after_toolcall 可在段内再改

未入内存的重新排序,第一条换成 io.setToolBlocks:它直接破坏“结果媒体能被
after_toolcall 就地改写”——插件只能推一份 base64 拷贝过去(ai_image 大图)。
2026-09-10 21:47:23 +08:00
b09f459aea docs(plan): 修正 §13.5 的错误判断,建账 §13.13 剩余内联路径
§13.5 之前写的「内核侧未接线」是方向性错误:我把分配方认错了。
实际是**插件侧** putInArena 做 arena.alloc + 写入 + 传 text_ref,内核侧
resolveText 读回。分配在插件侧并非缺口——§13.2 的模型就是「插件经 RPC
向内核申请/归还」,内核独占分配器;procCore.InjectText 拿到普通字符串
也是对的,因为共享内存是跨进程的内部实现、不对插件开发者暴露。

新增 §13.13,把「全量数据交互入共享内存」这个总目标的尾巴显式建账,
区分数据面(要入)与控制面(不该入,几十字节搬进去反而多两次 RPC):

已入:StageContext / 事件环 / tool.invoke / cleaner.invoke /
      output.invoke(本次 §13.6)/ io.injectText(§13.5)
未入:① 媒体块 io.injectMedia / injectMediaSync /
      injectInterruptMedia / setToolBlocks(本地大图 base64 可达数 MB,
      最大一条)② doc.insert / insertWithMedia ③ knowledge.add
      ④ 插件反向调内核读大结果仍内联
2026-09-10 21:34:42 +08:00
e2672c56d6 feat(shm): 输出通道 payload 走共享内存调用帧(§13.6)
§13.3 给 ToolInvokeParams 加了调用帧,但 OutputInvokeParams 没跟上——
payload 仍内联在 RPC JSON 里。核实后确认这个 lane 只做了半边:注入侧
(injectParams.TextRef + resolveText)可用,输出侧从内核到插件仍是内联。

改法照搬工具调用的 funccall 帧模型:

- OutputInvokeParams 加 Frame/ArgsLen(Args 仅留给直连 RPC 的测试)
- invokeOutput 序列化参数 → Alloc 帧 → 写帧 → 只传偏移描述符 → defer Free
- 帧尾不预留结果区:output 应答很小("ok"/status map),走 RPC 应答字段即可。
  但 OutputInvokeResult 支持插件把大结果写回帧(ResultRef),并识别
  sharedRefFlagExpand 单独归还扩容块——与 invokeTool 一致。
- 模板 output.invoke 用 frameInput 从帧读参数,无帧才回退内联 Args

为什么要做:output payload 在真机上可能含图片/文件描述等大字段,内联会
撑爆 stdin/stdout 管道。

验证:
- TestPlugin_OutputPayloadViaArena:9000 字节 payload 经帧完整送达(插件回报
  实收长度,不是只看返回值)+ 调用后 arena 归零
- TestE2E_RealTemplateOutputPayloadViaFrame:同上,但用真实 SDK 模板编译的
  插件——生产插件走的就是模板,模板不读帧则此改动等于没做
- TestPlugin_OutputChannelReportsRealFailure 仍绿:同步等真实结果、失败必须
  上报(§9.4)的语义未被破坏
- go test -race ./internal/plugin/... 全绿

plan.md §13.5 也如实标注:注入 side 内核→插件方向仍未接线(procCore.InjectText
直接传字符串,从不 Alloc/Put 构造 TextRef),不是仅打勾的项。
2026-09-10 21:28:56 +08:00
b71a88a471 chore(shm): 补齐 §13 验证项,删除统一区域遗留死代码
三件事,都是 plan.md §13 里未打勾的项:

1. §13.3「Bench: ToolInvoke 延迟对比」补测(bench_test.go)
   改造后原有的 BenchmarkToolInvoke 仍走 legacy 内联 Args,已不代表生产
   路径。拆成 inline / frame 两个子基准并各测两个尺寸(-count=3 取中位):

     payload      inline(RPC 报文)   frame(共享内存帧)   差
     16 B          26.4 µs/op           48.0 µs/op        +21.6 µs
     32 KiB       695.6 µs/op          391.7 µs/op       −303.9 µs

   取舍随尺寸翻转:小 payload 多付 ~22µs 帧固定开销,大 payload 省掉整份
   JSON 编解码与管道拷贝、快 44%。因为线上工具结果动辄几十 KB 且 22µs
   相对 LLM 往返 2-8 秒可忽略,所以统一走帧而不按大小分叉(§13.3 第 4 条)
   是对的。

2. 删除 §13.1 遗留死代码(evtring.go)
   allocEvtRing 自统一共享区域之后从未被调用——旧版它单独建 memfd + eventfd,
   现在只有一个 memfd,事件环只是区内的一个 segment(NewEvtRing 操作区内
   切片)。留着会让人误以为事件环还有独立段。

3. 修正误导性注释(plugin.go)
   仍写着旧 3-fd 布局「3=StageContext, 4=事件环, 5=通知」,与实现
   (procExtraFilesForShm 只返回 memfd+evtfd)和 shmpass_unix.go 的权威
   注释互相矛盾。

plan.md:§13.1/13.3/13.4/13.8 的验证项按实测打勾,并注明证据(提交号 /
测试名 / 实测数字),不留无依据的勾。
2026-09-10 21:18:16 +08:00
e218d0f100 fix(agent): 工具循环占位不再驱动重复发送,输出回执/子任务结果幂等
生产现象:单轮内 output_send__qq 被调用 34 次、持续 514 秒,直到 QQ 插件
自己的循环保险拒绝发送才停下(problem.md)。根因是多环节叠加,核心侧修四处:

1. 工具轮补位文案(process.go)
   通用占位「请根据以上工具结果继续。」对纯输出通道调用是错的:异步通道
   (qq/wechat)的回复只能经 output_send__* 交付,所以模型「已完成回复」的
   表达形式就是一个工具调用,紧随其后的「请继续」会被读成「还要再做一步」,
   而能做的「一步」恰好还是再发一条消息。
   改为按上一批工具的性质选文案:全部是 output_send__* 时补
   「若你的回复已完成,直接返回纯文本即可结束本轮,无需再调用任何工具。」
   同时每轮先移除旧占位再补一条,避免占位在 prompt 前缀里线性累积。
   (该占位是 zen 网关「最后一条必须是 user」的传输层附加物,HEAD 版本是
   无条件内联追加、从不移除。)

2. 输出成功回执(output.go)
   「已通过 [qq] 通道发送: map[status:sent]」这类富回执会被读成「这步成功,
   继续下一步」。成功改为只回极简标记。

3. proc 桥标量透传(internal/plugin/proc/plugin.go)
   插件返回 "ok" 时不再伪造 {status:sent} 覆盖插件真实返回值,否则只改
   output.go 不生效。

4. 子任务结果幂等(spawn.go)
   child_result 原先读到即删,而完成通知长期留在持久上下文里
   (formatMergedTimeline 每轮重新注入),第二次查询必然得到
   「不存在或已过期」这个永久失败信号,模型据此认为任务未完成而反复重试。
   改为保留结果 + delivered 标记,重复查询返回明确提示;结果按上限有界淘汰。

顺带:agent.go 去掉文档层显式向量器注入(TF-IDF 已内置为 fallback),
cmd/homed/main.go 同步 document.NewStore 的 tokenizer 参数。

测试:internal/agent/core/tooloop_test.go(5 例)、spawn_test.go(3 例)。
2026-09-10 20:36:39 +08:00
b096e8ba2c refactor(shm): 变长块分配器 + 工具调用 funccall 调用帧(§13.2/§13.3)
按架构约束推进两步:

1) 分配器收回内核后,定长槽失去了唯一存在理由
   (定长槽只是为了绕开"跨进程无法安全变长分配"),
   于是 arena 换成**变长块分配器**:first-fit + 邻块合并,
   块头 16B(size/state/owner/prevSize)。内核可以按需标定每块大小,
   payload 不再受固定槽容量限制(旧上限 16KB)。

   - Alloc/Put/Read/Free/ReclaimOwner 全在内核进程内,一把 sync.Mutex
   - Free 走块链校验 offset 是已分配块的数据起点 + owner 匹配,
     伪造引用不能改动分配器状态
   - Read 允许块内偏移(调用帧的结果区就在帧块中间),但不许跨块边界
   - arena 4MB,底层 memfd 惰性分配:未触碰的页不占物理内存

2) 工具调用 payload **始终**走共享内存,取消按大小切内联的分支。
   按 funccall 模型,内核(caller)标定调用帧交给插件(callee):

       [0, ArgsLen)              参数 JSON
       [ArgsLen, Frame.Length)   结果区(内核预留的预算)

   结果超出预算时插件才 `arena.alloc` 扩容块,引用上打
   sharedRefFlagExpand 让内核单独归还。Cleaner 复用同一帧模型。
   ToolInvokeParams.Args / ToolInvokeResult.Result 仅剩给直连 RPC 的
   测试(process/bench 不建 Host);生产路径永远走帧。

测试:
- TestArena_*(分配/归属/回收/并发唯一/耗尽/超限/伪造 offset/
  相邻合并/对齐/布局校验)
- TestPlugin_ToolInvokeArgsResultViaArena:大**小** payload 都经帧往返,
  结果一致且 arena 归零("小 payload 同样走调用帧"是本轮行为变更)
- TestPlugin_ArenaAllocFreeAcrossProcess、TestE2E_* 保持通过
- 顺手修 process_test.go 一处预存 doc-comment 分段
2026-09-10 18:54:36 +08:00
f6f92a8a6e fix(plugins): 修复两处预存缺陷(agentcli 临界区 / localuse 脚本转义)
agentcli cleanupLoop:
持锁期间为每个过期终端 go func 调 term.Close()(内部会 Kill 进程并等
<-t.done),临界区被拉长且与 TerminalSession 退出路径交错。改为持锁
只做筛选与摘除,锁外再逐个关闭。

localuse 脚本转义(两处真实 bug):
1. AppleScript 注入:handleScreensue 只把 `"` 转义为 `\"`,未转义 `\`。
   内容结尾的反斜杠会吃掉闭合引号,使后续内容逃逸出字符串字面量。
   改为 escapeAppleScriptString:**先转义反斜杠再转义双引号**(顺序
   不可颠倒,否则刚插入的反斜杠会被二次转义)。
2. PowerShell 路径被错误加倍反斜杠:handleScreensee 把 Windows 临时
   路径的 `\` 写成 `\\`。但 PowerShell 单引号串里反斜杠是普通字符
   (转义符是反引号),加倍会让截图保存到错误路径。
   新增 psSingleQuote 统一处理,并把 4 处零散的 `ReplaceAll(x,"'","''")`
   收敛到该 helper。
2026-09-10 17:56:57 +08:00
c03de9878d fix(proc): EvtConsumer 生命周期——消除 host.Close 后的 SIGSEGV
预存缺陷(非本次重构引入,但会稳定复现崩溃):

Stop() 只 close 了 stop channel,而 Run() 阻塞在 evtfd.Read 里,
根本没有机会检查 stop。调用方在 Stop 后释放 ringData(host.Close
会 munmap 整个区域),Run 一旦从 Read 恢复就会读已解除映射的内存:
**SIGSEGV,recover 捕不到**。实测 TestEventRing_OverflowStillDelivers
约 50% 概率触发。

两次尝试与结论:
1. os.File.SetReadDeadline 无效——eventfd/pipe 经 os.NewFile 包装后
   **不会**注册进 Go netpoller(os.NewFile 对非 open 得到的 fd 一律按
   非 pollable 处理),Read 是阻塞 syscall,SetReadDeadline 返回错误。
2. 改为 poll(2) 显式加超时(evtpoll_unix.go),消费循环每 100ms 回到
   stop 检查。

新增 API 契约:
- Stop() 非阻塞,仅请求退出
- Wait() 阻塞至 Run 退出;**返回后才能释放 ringData**
- drainEvents 每条事件后检查 stop,避免慢 handler 拖延退出

其他:
- evtring_test.go 三个用例改为 defer Wait() → defer Stop()(LIFO 保证
  Wait 先于 host.Close 完成)
- 溢出用例的 handler 改为非阻塞投递:写入了 8292 条事件而 channel 只
  消费 1 条,阻塞投递会让 drainEvents 卡在 handler 里,Stop 无法退出
2026-09-10 17:56:54 +08:00
ca584bda3f refactor(shm): 内核独占共享槽池,插件经 RPC 申请/归还(§13.2/§13.3)
推翻前两版跨进程分配器设计,根因是"共享内存里放了只被单一进程
更新的可变游标":

- v1 在 SuperBlock 放 arenaUsed 游标,内核 CAS bump。但插件模板里
  arenaUsed 是**进程本地变量**,两进程各自 bump 必写同一段内存;
  arenaReset 还会重置共享游标覆盖对方数据。
- v2 把位图 CAS 下沉到插件模板,正确但把分配器实现泄漏进插件运行时,
  且插件必须与内核保持位图布局同步。

新设计(用户明确的架构约束):内核全权管理共享内存,插件通过
syscall 风格 RPC 申请/归还,内核返回偏移与大小。分配器只存在于内核
进程内,一把 sync.Mutex 即可。共享内存是内部实现,不对插件开发者
暴露——SDK 公开 API 仍是普通字符串/Map。

主要改动:
- arena.go: 定长槽 + 位图,Alloc/Put/Read/Free/ReclaimOwner,内核独占;
  槽头记录 owner,Free 校验归属;payload 超槽容量退回内联 RPC
- protocol.go: 新增 arena.alloc / arena.free(CapCore)
- capability.go: 登记两个新 method(checkAllMethodsClassified 要求)
- corehandler.go: 实现 arena.alloc/free;Cleaner 改为内核预分配
  请求槽 + 响应槽(插件完全不分配)
- plugin.go: Start 领 ownerID;handleExit 调 ReclaimOwner 回收残留槽
- unified.go: 移除坏的 bump 分配器;arena 基址 8 字节对齐
- 删除 toollane.go: ring 状态机是死代码(从未接线),且把 RPC 已有的
  请求 ID 关联/错误传递/ctx 取消重新实现了一遍。控制面保留 RPC,
  只把 payload 搬进共享槽。
- 测试: TestArena_*(归属/回收/并发唯一/耗尽/超限/非法引用/布局)+
  TestPlugin_ArenaAllocFreeAcrossProcess(真进程申请→写入→随业务
  RPC 回传→归还→内核读回一致且池归零)
2026-09-10 17:56:50 +08:00
cf3c99233b fix(shm): 审核修复——arena 并发安全 + ToolCall ring 抢占 + arenaRead 校验
§13.2: arenaAlloc 改 CAS bump(修复并发覆盖);arenaRead 增加 generation + arena 边界校验
§13.3: Reserve 从 Store+Load 改 CAS-free→Reserved 状态机抢占帧(修复并发分配重复);
  增加 RESERVED 中间态;SetReading/SetReady 改 CAS 返回 bool;ReleaseFrame 清零跳过 state
§13.5/13.6: 注入方法接入 resolveText(从 arena 读 SharedRef);cleanerProxy 加 arenaMu 串行化+每次重置
§13.8: ContextPolicy prune topK 改为 maxContextSize-1(不再硬编码 20)
  tool.register 校验 context_policy 只允许 none/prune
2026-09-10 17:03:32 +08:00
2bc813be13 feat(shm): InputChannel/OutputChannel lane 基础设施(§13.5/§13.6)
- injectParams 加 TextRef SharedRef(兼容旧 Text 字段)
- resolveText 辅助函数:优先 SharedRef,否则内联 Text
- 核心注入方法待迁移至 resolveText
2026-09-10 16:14:17 +08:00
9cfdfc9de8 feat(shm): arena + ContextPolicy(§13.2/§13.8)
- unified.go: arena 分配器 (alloc/write/read/reset)
- NewHost: 分配空间含 arena
- SDK ToolDef 加 ContextPolicy string
- StageAfterToolcall 后检查 prune 策略触发 RelevanceContext.Prune
2026-09-10 15:49:47 +08:00
772a494223 feat(ctx): ContextPolicy tool 上下文策略(§13.8)
- SDK ToolDef 加 ContextPolicy string(默认 none / 可设 prune)
- StageAfterToolcall 后检查工具 ContextPolicy=prune → RelevanceContext.Prune
- 所有现有测试通过
2026-09-10 15:41:23 +08:00
5608abdf5a feat(shm): Cleaner 迁移到 SharedRef(§13.4)
- CleanerInvokeParams/Result 的 Text 改为 TextRef SharedRef
- cleanerProxy: 写 text 到 arena,传 SharedRef,读结果 SharedRef
- 插件侧 cleaner.invoke: 从 SharedRef 读 input,清洗后写回 arena 返回 TextRef
- NewHost(): 分配空间含 arena(256KB)
2026-09-10 15:30:00 +08:00
cf098a1d4f feat(shm): arena allocator for unified region (§13.2)
- SuperBlock 新增 arenaOff/arenaCap/arenaUsed 字段
- arenaAlloc: append-only bump 分配,offset 0 保留给空语义
- arenaWrite: 写入并返回 SharedRef(含 generation)
- arenaRead: 按 SharedRef 切片读取
- arenaReset: 压实后重置游标
2026-09-10 14:33:44 +08:00
a8e3633174 feat(shm): ToolCall ring buffer(§13.3) — 栈帧模型 ring,push/pop 帧状态机
- ToolCallRing: 定长帧 ring buffer (64 frames × 112B),状态机 FREE→CALLING→READING→READY→FREE
- Reserve: 环形扫描找 FREE 帧,全忙返回 ErrToolCallRingFull 背压
- SharedRef: pack/unpack 辅助函数
- unified.go: arena 分配器(arenaAlloc/arenaWrite/arenaRead)
- 5 个单测覆盖 init/reserve/背压/find/reuse
2026-09-10 12:33:58 +08:00
ad016e4410 feat(shm): 统一共享内存区域(§13.1) — 单 memfd 容纳 SuperBlock+StageContext+EvtRing
- unified.go: SuperBlock 布局 + SharedRef 类型
- NewHost(): 两段 memfd 合并为单一 memfd,fd 3 = 统一区域,fd 4 = eventfd
- 子进程侧: testdata 插件从 SuperBlock 解析 ctxOff/evtOff
- streaming 测试: EvtConsumer 协程在 host.Close 前 Stop+Wait 防 SIGSEGV
2026-09-10 11:42:22 +08:00
7a328679da feat(proc): Cleaner 跨进程修复(tool/input/output) + 共享内存安全模式 + plan §13
- Cleaner 协议统一为 cleaner.invoke(scope,name,text),覆盖工具/输入/输出三类
- 新增 ShmSecurityMode (safe/debug/full),Linux 审计探针,Windows crypto nonce
- plan.md 追加 §13 步骤分组
2026-09-10 10:21:50 +08:00
580d5f5501 feat(doc): dense vector index for unified text+media retrieval
文档层引入稠密向量索引,与媒体检索共享同一多模态空间:
- Doc 加 DenseVec 字段(json:-,运行时计算)
- Consume/QueryScored 优先使用 denseSearchScored(brute-force cosine),
  未配置时退化到 TF-IDF 倒排检索
- buildDenseIndex 在 Agent 启动时为全部文档一次性计算稠密向量
- L0 RelevanceContext 支持 denseSpace(Prune 使用稠密余弦),
  退化到 fastText 稀疏余弦

vector 包新增 DenseCosine([]float64 brute-force cosine)。

验证:492 篇文档 brute-force ~300ms,全部测试通过。
2026-09-09 18:56:44 +08:00