OpenViking ZCode 记忆插件设计解读基于扩展面验证的七事件 Hook 适配与演进式会话捕获【免费下载链接】OpenVikingSelf-evolving Context Database for AI Agents. Unify Agent Memory, Knowledge RAG and Skills.项目地址: https://gitcode.com/GitHub_Trending/op/OpenViking导读本文基于 examples/zcode-memory-plugin/DESIGN.md 与配套的 README.md完整解析 OpenViking 为 ZCode 编码助手构建的长期记忆适配层。文章先梳理 ZCode 扩展面的 7 个可验证事实再深入四个关键设计决策运行时打包、配置级 Hook、四事件裁剪、严格 JSON 输出契约最后结合zcode-capture.mjs、zcode-turns.mjs等源码剖析 rollout 优先的增量会话捕获与去重机制。读完本文你将掌握ZCode Hook 事件与模板变量的真实支持范围、配置级 Hook 的落地方式以及如何在缺失PreCompact/SessionEnd的前提下用Stop事件补偿完成可靠的记忆提交。阅读前提本插件运行于已安装 OpenViking 服务的环境并要求 OpenViking 服务端支持viking://~主页别名home-alias。Recall 通过viking://~/memories与viking://~/skills定向到调用者自身的上下文空间新版服务端会拒绝无 uid 的viking://user/memories简写形式。一、ZCode 扩展面事实清单先验证再设计ZCode 记忆插件的设计建立在一组经过实测验证的扩展面事实之上——这些事实并非来自文档假设而是通过真实 ZCode 安装环境内置zcode-guide插件文档、实际~/.zcode/cli/config.json、以及安装了真实钩子的插件逐条核实而来。这张表是整个插件设计的基石方面已验证事实支持的 Hook 事件SessionStart、UserPromptSubmit、PreToolUse、PermissionRequest、PostToolUse、PostToolUseFailure、Stop恰好 7 个不支持的事件PreCompact、SessionEnd、Notification、SubagentStart、SubagentStopManifest 探测顺序.zcode-plugin/plugin.json→.claude-plugin/plugin.json→.codex-plugin/plugin.json模板变量插件 Hook${CLAUDE_PLUGIN_ROOT}、${ZCODE_PLUGIN_ROOT}、${CLAUDE_PROJECT_DIR}、${ZCODE_PROJECT_DIR}、${CLAUDE_SESSION_ID}模板变量配置级 Hook无——配置文件中的 Hook不展开模板Hook 输出 schema严格 JSON——任何多余键都会导致校验失败输出被整体丢弃MCP 配置位置~/.zcode/cli/config.json→mcp.servers用户作用域插件 MCP 命名空间plugin:plugin:serverMCP 自动连接所有作用域在会话启动时自动连接Hook runner 启用任一插件贡献 Hook 时自动启用超时单位command类型timeout单位为秒process类型timeoutMs单位为毫秒async字段无运行时效果——Hook 始终内联执行这 12 行事实直接决定了后续所有设计取舍。尤其值得注意的是两点严格 JSON 输出 schemaZCode 解析 Hook stdout 时要求严格 JSON任何未识别的键都会导致整个输出被静默丢弃。这意味着插件绝不能输出 Claude Code 风格如{ decision: approve }的字段——这是对抗性评审中排名第一的静默失败模式见 DESIGN.md 决策 4 的 provenance R1-F1、R4-V1。配置级 Hook 不展开模板变量这与插件级 Hook 形成鲜明对比。若插件把${ZCODE_PLUGIN_ROOT}直接写进~/.zcode/cli/config.json路径将永远不会被替换。设计文档专门为此给出了对策安装期由install.sh的renderHookCommand()完成绝对路径替换见下文决策 2。二、四个关键设计决策决策 1内置共享运行时vendor而非相对路径引用与 TRAE/Cursor 采用跨目录相对路径导入共享库不同ZCode 插件采用与 Claude Code、Codex 相同的模式把共享运行时整体 vendoring 进scripts/shared/使插件自包含、可整体搬移。这一取舍的 provenance来源依据是对抗性评审 B1vendor 与相对路径两种方案互斥必须二选一。最终选择纯 vendor是因为 ZCode 基于配置驱动的安装模型会把文件拷贝到~/.openviking/agent-integrations/——跨目录相对路径在这种拷贝式安装下极易断裂。仓库中的实际布局印证了这一点examples/zcode-memory-plugin/scripts/shared/ 下共有 18 个共享模块agent-hook-runtime.mjs、recall-core.mjs、pending-queue.mjs、mcp-proxy-core.mjs、credentials.mjs等它们与 Claude Code、Codex、Cursor 等插件复用同一套memory-plugin-shared运行时不复制任何记忆逻辑。ZCode 适配器本身只新增薄薄一层分发器zcode-hook.mjs、捕获逻辑zcode-capture.mjs、transcript 解析器zcode-turns.mjs以及三个设置了环境变量后导入分发器的薄 shim 脚本。决策 2配置级 Hook而非插件清单 Hookinstall_zcode()把 Hook 与 MCP 配置写入~/.zcode/cli/config.json配置文件作用域而不是走插件市场注册流程——这与 Cursor/TRAE 的安装模式一致。关键在于解决配置级 Hook 不展开模板变量的限制源文件 hooks/hooks.json 中写的是${ZCODE_PLUGIN_ROOT}占位符而install.sh的renderHookCommand()会在安装期将其替换为绝对路径。相关实现位于 examples/memory-plugin-shared/install.sh它用正则/^node\s?\$\{(?:CLAUDE_PLUGIN_ROOT|ZCODE_PLUGIN_ROOT)\}?\/(.?)?$/u匹配命令模板再逐层递归渲染所有command键见renderHookValue。另一个易踩的坑同样源于对抗性评审 R4配置级 Hook 要求hooks.enabled: true合并脚本会自动设置这一项。此外 README 还强调一个不变式Hook 组必须省略 matcher 键而不是写matcher: ——严格解析器会把空字符串视为非法值并可能静默丢弃整个配置来源。观察 hooks/hooks.json 可以看到SessionStart、UserPromptSubmit、Stop三个事件组都没有 matcher 键只有PreToolUse组显式声明了matcher: Read|Glob|Grep。决策 3只用四个事件——ZCode 支持子集的裁剪ZCode 支持 7 个事件但不支持PreCompact、SessionEnd、SubagentStart、SubagentStop。因此插件只接 4 个事件事件匹配器命令timeout秒SessionStart—node ${ZCODE_PLUGIN_ROOT}/scripts/session-start.mjs30UserPromptSubmit—node ${ZCODE_PLUGIN_ROOT}/scripts/auto-recall.mjs20PreToolUseRead\|Glob\|Grepnode ${ZCODE_PLUGIN_ROOT}/scripts/uri-guard.mjs5Stop—node ${ZCODE_PLUGIN_ROOT}/scripts/auto-capture.mjs30commit-on-Stop策略既然没有PreCompact/SessionEnd来标记上下文压缩与会话收尾插件把提交 OpenViking 会话的动作压到Stop事件上完成。而且Stop的父进程在读取 stdin之前就脱附detach到独立工作进程保证网络写入不会阻塞 ZCode 本体。这四个事件名称都经过对抗性评审 R1 验证合法R4 同时确认不支持的额外事件会被静默丢弃这正是必须只用支持子集的原因。决策 4只输出 ZCode 标准键ZCode 的严格 JSON schema 会拒绝任何未识别的键。因此分发器zcode-hook.mjs只输出两类内容上下文注入{ hookSpecificOutput: { hookEventName: ..., additionalContext: ... } }用于SessionStart/UserPromptSubmit放行透传stdout 为空字符串 退出码 0。而PreToolUse的拒绝路径由独立的 scripts/uri-guard.mjs 处理输出形如{ hookSpecificOutput: { hookEventName: PreToolUse, permissionDecision: deny, permissionDecisionReason: ... } }全程不出现decision: approve这类 Claude Code 习惯字段。从源码看uri-guard.mjs的evaluateZcodeUriGuard()兼容多组字段名tool_name/toolName/name/tooltool_input/toolInput/input把检测委托给共享的evaluateAgentUriGuard(toolName, toolInput)若未命中viking://URI则输出为空对象、静默放行。附加决策.zcode-plugin/plugin.json优先Manifest 探测顺序是.zcode-plugin/→.claude-plugin/→.codex-plugin/。虽然.claude-plugin/也能工作但.zcode-plugin/plugin.json是文档明确的首选位置对抗性评审 R1。本仓库的 openviking.integration.json 即为该插件的集成清单。三、安装与事件流从共享安装器到四个 Hook安装方式使用共享安装器指定--harness zcodebash examples/memory-plugin-shared/install.sh --harness zcode安装器通过~/.zcode/目录或zcode二进制检测 ZCode见 install.sh然后把 Hook 与 MCP 配置合并进~/.zcode/cli/config.json、把 MCP 服务器写到~/.zcode/mcp.json、并把 OpenViking 凭据写入~/.openviking/ovcli.conf。zcode_merge_config()install.sh负责三份 JSON 的合并安装完成后还会做自检检查scripts/session-start.mjs、auto-recall.mjs、auto-capture.mjs、uri-guard.mjs、OPENVIKING_INTEGRATION_ID、mcp-proxy.mjs是否都已出现在配置中见 install.sh。四个 Hook 的事件流SessionStart —— 注入用户画像。session-start.mjs设置OPENVIKING_HOOK_EVENTsession-start后导入分发器。分发器在带锁临界区内先检查与上次注入的间隔小于 2 秒直接跳过防止重复触发随后replayAgentPending()重放待发送队列、buildAgentProfile()拉取用户画像与偏好/实体最终以openviking-context sourcesession-start.../openviking-context形式注入上下文。UserPromptSubmit —— 记忆召回。auto-recall.mjs设置OPENVIKING_HOOK_EVENTuser-prompt-submit。分发器对用户 prompt 计算stableHash并配合promptEventIdgeneration_id/request_id/message_id/prompt_id任一可用做去重命中缓存块则直接复用state.recallBlock否则调用recallForPrompt()在 OpenViking 中检索相关记忆输出relevant-memories.../relevant-memories注入。PreToolUse —— URI 守卫。匹配Read|Glob|Grep三类工具拦截对viking://URI 的直接读取强制 Agent 改走 OpenViking MCP 工具命名空间plugin:plugin:server即plugin:openviking:openviking从工具层保证记忆数据的访问受控。Stop —— 增量捕获与提交。auto-capture.mjs设置OPENVIKING_HOOK_EVENTstop。run()先调用maybeDetach()把捕获逻辑丢进脱附的独立进程父进程不读 stdin网络写入不阻塞 ZCode随后构建捕获计划、addAgentMessages()增量写入、必要时commitAgentSession()提交会话并推进持久化的光标状态。Stop 事件本身无需任何 stdout 输出透传。注timeout单位为秒如 Stop 30 秒async字段没有运行时效果——所有 Hook 始终内联执行脱附是插件自己在Stop分支里实现的。四、rollout 优先的增量会话捕获主 transcript 与去重设计ZCode 的StopHook stdin 字段并未完整公开文档化。设计文档基于 ZCode 源码逆向#3127作者 quinn-zenith与 rollout 文件分析确认了以下事实Stop载荷至少包含session_id/sessionId、cwd、transcript_path指向只含最后一条 assistant 消息的临时文件并非完整对话、responseText/responsePreview最后一条 assistant 响应文本用户内容不在 stdin 中完整对话位于 rollout 文件~/.zcode/cli/rollout/model-io-sessionId.jsonl每行一条 JSON{ sessionId, turnId, type: model_io, request: { messages: [...] }, response: { text } }。因此zcode-turns.mjs采用** rollout-first 策略**rollout 文件可读时readUnseenRolloutTurns()从lastKnownTurnId之后增量解析全部未见 turn若无lastKnownTurnId首次捕获则捕获全部条目而不丢历史。每条 rollout 条目同时产出 user turn取request.messages中最后一条 user 角色消息兼容字符串与多模态块两种 content 形态和 assistant turn取response.text。stdin 仅是兼容性兜底rollout 不可用时从responseText/responsePreview/last_assistant_message等字段猜 assistant 内容从prompt/user_prompt/userMessage或state.pendingPrompt猜用户内容。清洗cleanZcodeText()会剥掉插件自己注入的openviking-context、relevant-memories、system-reminder块再入记忆避免把注入内容当成对话内容存回去。zcode-capture.mjs负责去重与确认游标其核心机制去重键zcodeTurnDedupKey()优先用稳定的宿主turnId组成${turnId}:${role}无turnId时才退化为内容哈希stableHash(role, content)。键始终基于原始 turn 计算因此即使调大captureMaxLength也不会把服务端已持有的截断版本重新发送一遍。增量发送buildZcodeCapturePlan()对每个候选 turn 先过shouldCaptureText()长度、角色等过滤再剔除capturedTurnIds中已确认的键剩余才是toSendpayload 附带turn_id原样透传。确认游标applyZcodeCaptureResult()按实际已发送数 已入队数计算captured只有已发送或已持久入队的消息才写入确认集合acknowledgedCursor()沿候选列表推进lastTurnId——只有某个turnId的 userassistant 全部确认时才推进游标。这保证即使某次Stop重复投递或丢失下一次Stop也能从 rollout 中恢复错过 turnturnId单调递增rollout 是权威增量来源。pendingPrompt 联动若 pending prompt 对应的 user turn 已被确认则清空pendingPrompt状态capturedTurnIds只保留最近 1000 个键slice(-1000)防止状态无限膨胀。五、对抗性评审的融入与回归测试DESIGN.md 明确交代了每个决策的 provenance来源依据体现设计即评审记录的风格评审结论落点B1vendor 与相对路径互斥决策 1 选择纯 vendorR4配置级 Hook 需hooks.enabled: true决策 2合并脚本自动设置R1四事件名称合法决策 3R4不支持事件被静默丢弃决策 3只接支持子集R1-F1 / R4-V1输出 schema 严格多余键整体丢弃决策 4只输出标准键聚焦回归套件覆盖了rollout 优先恢复、确认与游标状态、重复Stop投递、脱附慢写入、以及从与 TOS 发布流程相同的市场暂存脚本安装。测试可直接运行node --test examples/zcode-memory-plugin/scripts/*.test.mjs仓库中的测试文件包括zcode-capture.test.mjs捕获计划与确认游标、zcode-turns.test.mjsrollout 解析与字段兼容、zcode-hooks.test.mjs事件分发与zcode-async.test.mjs脱附与异步写入。六、已知未决问题Primary unknownsDESIGN.md 诚实列出了尚待实测确认的点理解它们有助于你判断当前实现的可依赖边界Hook stdin 字段名Stop暴露的responseText/responsePreview等字段来自源码逆向用户内容不在 stdin 中需要 rollout 兜底输出 schema 接受度hookSpecificOutput包装层是否能被 ZCode 原样接受仍需在真实 ZCode 会话中验证MCP 工具名格式命名空间为plugin:openviking:openviking需确认工具名与预期一致Turn 身份rollout 条目携带单调递增的turnIdrollout 可读时是权威增量来源stdin 仅为兼容兜底。适配器把该身份透传为 OpenViking 的turn_id仅在消息已发送或已持久入队后记录角色去重键且只通过完整确认的 rollout 条目推进lastTurnId。七、设计要点小结验证驱动设计先对真实 ZCode 环境盘点扩展面7 事件、模板变量、严格 JSON schema、超时单位等 12 项事实再谈方案配置级 Hook 安装期模板渲染利用renderHookCommand()规避配置级 Hook 不展开模板的限制保证${ZCODE_PLUGIN_ROOT}落地为绝对路径少即是多7 个支持事件只接 4 个缺失的PreCompact/SessionEnd由Stop上的 commit 与脱附写入补偿rollout 权威 stdin 兜底以宿主turnId驱动去重与游标推进容忍重复Stop与漏投递严格输出契约只输出 ZCode 识别的键杜绝 Claude Code 风格字段导致的静默丢弃。该插件与 examples/claude-code-memory-plugin、examples/codex-memory-plugin、examples/cursor-memory-plugin 等共享同一套memory-plugin-shared运行时——ZCode 只是其薄适配层之一。若你想把该适配模式复用到其他编辑助手zcode-turns.mjs的字段兼容层多候选字段名 rollout 权威源是最值得参考的样板。【免费下载链接】OpenVikingSelf-evolving Context Database for AI Agents. Unify Agent Memory, Knowledge RAG and Skills.项目地址: https://gitcode.com/GitHub_Trending/op/OpenViking创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考