NeMo Relay 共享指标
在 Relay 发布原生 wheel 的平台上,Hermes 把 NeMo Relay 作为正常运行时依赖纳入。共享指标集成内置于 Hermes,不需要额外安装 Hermes 可观测性插件。在其他原生目标上,Hermes 在没有 Relay 的情况下仍可导入。这些目标使用一个显式的、能力缩减的 no-op 宿主:Hermes 执行仍然可用,而 Relay 作用域、中间件、插件和订阅器不可用。hermes-agent[nemo-relay] extra 作为 no-op 兼容别名保留,供既有安装命令使用。
[!WARNING] 这会移除 Hermes 的
observability/nemo_relay插件。既有用户必须从plugins.enabled中移除observability/nemo_relay(或其旧版别名nemo_relay),并把导出器配置迁移到 Relay 的plugins.toml。HERMES_NEMO_RELAY_PLUGINS_TOML可指定一个显式文件;迁移旧的HERMES_NEMO_RELAY_ATOF_*和HERMES_NEMO_RELAY_ATIF_*设置时,hermes update或hermes migrate relay会创建该文件。这些旧变量不再用于配置 Relay 导出器本身。
在受支持平台上,Hermes 要求 NeMo Relay 0.9,以进行托管提供商和工具调用。
运行时依赖与数据边界 {#runtime-dependency-and-data-boundary}
Hermes 从有界的 >=0.9,<0.10 依赖范围安装平台专属的 nemo-relay 原生 wheel。发布的软件包构建自 NVIDIA NeMo Relay 仓库。不受支持的平台使用上文所述的显式 no-op 运行时,而非下载另一种实现。
当 Relay 托管执行处于活动状态时,提供商请求与响应会经过 Hermes 进程中的该原生模块,以便配置的拦截器能作用于真实调用。这与共享指标数据契约是两回事。共享指标模式不安装富可观测性网络导出器,其订阅器只接受下文所述、带版本号且经过白名单的投影。附录 A 所述的可选软件包发送器是唯一的出口路径,只有在用户同时开启 enabled 和 send 时才传输内容,且发送的是整包而非实时 span。启用一个单独配置的富可观测性或动态插件,可能形成不同的数据路径,并需要单独的策略评审。
采集默认关闭,除非 Hermes 策略开启:
telemetry:
shared_metrics:
enabled: true
该选择从 profile 自己的 config.yaml 读取。机器托管的配置覆盖不能代替 profile 开启或关闭共享指标。
Hermes 使用 Relay 正常的进程级插件发现。Relay 按优先级从低到高读取以下文件:
| 层 | Linux 与 macOS | Windows |
|---|---|---|
| 用户 | $XDG_CONFIG_HOME/nemo-relay/plugins.toml,或 ~/.config/nemo-relay/plugins.toml | %USERPROFILE%\.config\nemo-relay\plugins.toml(设置了 XDG_CONFIG_HOME 再到 HOME 时优先) |
| 系统 | /etc/nemo-relay/plugins.toml | %ProgramData%\nemo-relay\plugins.toml |
HERMES_NEMO_RELAY_PLUGINS_TOML 用一个显式文件替换用户文件;系统文件仍在其之上生效。仓库本地配置被忽略。若显式选定的文件无法加载,Hermes 会报告错误并在不加载 Relay 插件的情况下继续,而非回退到另一配置。
运行 hermes doctor 查看哪些文件生效。其 NeMo Relay Plugins 小节列出 Relay 解析的每个文件、是否启用了任何插件,以及 Relay 报告的任何问题,但不加载插件代码。
连续会话的会话 span 分段 {#session-span-segmentation-for-continuous-sessions}
Relay 在其作用域关闭时导出一个 span。一个连续的网关会话可能开启数天,因此其会话 span 保持开启,即使每个轮次 span 都正常导出。可选分段只在轮次边界轮换会话作用域:
gateway:
telemetry:
session_segments:
on_compaction: false # 上下文压缩后轮换
max_turns: 0 # 0 = 不限;N = 每段轮次数
| 键 | 默认 | 行为 |
|---|---|---|
on_compaction | false | 在压缩完成后、下一个轮次边界轮换。 |
max_turns | 0 | 每完成 N 个轮次后轮换;0 关闭上限。 |
两个默认值都为整个会话保留一个会话作用域。轮换出的 span 保留相同的 session_id,并附加 hermes.session.segment 以及 hermes.session.segment_reason(compaction 或 max_turns)。
工作目录作用域数据 {#working-directory-scope-data}
当 Hermes 知道一个会话或任务的逻辑工作目录时,其 hermes.session 和 hermes.turn 起始作用域会在 ATOF 中以 data.cwd 包含它。因此,在任务工作树中运行的轮次可能与其所属会话不同。未知目录被省略,作用域结束数据保留给结果。
工作目录是 Relay 作用域输入,因此对每个已启用的 Relay 订阅器可见,而不仅是 ATOF。路径可能暴露用户名、仓库名或挂载布局。Relay 不按工作目录过滤事件;若某路径不得离开主机,请使用可信的本地采集器,或不要为该进程启用远程导出器。
进程级插件策略与 profile 隔离 {#process-wide-plugin-policy-and-profile-isolation}
Relay 插件配置是进程级部署选择,不是 Hermes profile 设置。第一个托管 profile 触发惰性初始化,此后该 Hermes 进程托管的每个额外 profile 都共享由此产生的静态中间件、动态插件、订阅器、导出器和护栏策略。初始化成功后,Hermes 记录它加载的文件:
Relay 插件宿主已在进程级激活,并适用于本 Hermes 进程托管的所有 profile。配置文件:/home/user/.config/nemo-relay/plugins.toml; /etc/nemo-relay/plugins.toml
在该共享策略内,profile 作用域仍保留因果隔离。ATIF 按其顶层 Agent 作用域分组事件,因此同时进行的各 profile 会话产生独立轨迹,而非一条混合轨迹。ATOF 和其他全局订阅器观察来自每个被托管 profile 的事件。静态和动态中间件同样为来自每个 profile 的托管调用运行。
在独立 worker 进程中运行的 worker 插件不构成按 profile 的安全边界。一次进程级激活会把来自所有被托管 profile 的调用分派给该 worker,同时保留发起调用 profile 的 Relay 作用域栈。原生动态插件加载进 Hermes 进程,共享同一策略边界。
当各 profile 需要不同的信任级别、插件凭据、导出器目的地或护栏策略时,请在独立的 Hermes 进程中运行它们。本进程级插件契约不改变每个 profile 独立的共享指标同意、本地 SQLite 状态或 ATIF 轨迹分组。
Hermes 核心为每个 Hermes 会话持有一个 Relay 宿主和一个隔离的 Relay 会话作用域。核心生命周期生产者使用 agent.relay_runtime 获取共享会话句柄,或在该会话上下文中运行 Relay 作用域、LLM、工具和 mark API。新产品 mark 不需要 Hermes 插件注册。共享指标 mark 仍只能包含经版本化白名单批准的字段;硬依赖不改变采集或隐私策略。
当前切片 {#current-slices}
当前纵向切片记录假名化的 profile 活动、逻辑模型调用、顶层任务运行、工具与审批结果,以及 skill 生命周期与复用:
Hermes 轮次、API、工具和审批钩子
-> Relay 会话、任务、LLM、工具和 mark 生命周期
-> Hermes 共享指标订阅器
-> SQLite 计数器
-> 不可变 JSON delta 包
Hermes 向指标所属的生命周期送入一个空的 LLMRequest。它不描述上文记录的、经原生运行时的那条独立托管执行调用。终端指标事件包含 Hermes 为该逻辑调用使用的模型标识符和提供商路由,例如经 openrouter 调用 nvidia/nemotron-3-ultra。这些标识符被小写并做结构上有界的处理,但不经过一份入库的模型目录归一化。定价和模型族分类属于指标后端。提示词、响应、端点、错误文本、会话 id、任务 id 和请求 id 不包含在指标事件或软件包中。新调用使用 hermes.model_route.count。自软件包 schema v3 起,每条路由行还携带 call_role(primary 或 auxiliary)、outcome(success、failed、cancelled)和 error_class:即该逻辑调用最后一次失败尝试时,错误分类器自己的 FailoverReason 值(rate_limit、auth、context_overflow……),或 none。一个 success 行若带非 none 类别,表示该调用在该错误后恢复。早先的 hermes.model_call.count 契约仍可读,仅为让旧构建创建的待处理本地计数器能在不丢数据的情况下导出。
首个获得同意的会话开始时,发出一个空的 hermes.client.active Relay mark。按 profile 的订阅器创建一个随机 UUID 安装身份,并使用事务性 compare-and-set,在任意滚动 24 小时窗口内至多记录一条 client-active 计数器。该指标无维度;Hermes 版本、操作系统族、架构和安装方式仍是有界的包资源。并发的 Hermes 进程共享 SQLite 闩锁,因此同时开始不会让一次安装被重复计数。后续会话或任务可再次尝试该 mark,但订阅器会抑制它,直到滚动窗口过期。
每次任务运行是一个名为 hermes.task_run 的 Relay Function 作用域,挂在所属 Hermes 会话之下。起始计数器只含有界的执行表面和入口值;对网关任务,还含内建消息 platform(telegram、discord、slack……;Hermes 在 plugins/platforms/ 下按名发布的平台、仅当安装器自己的记录证明是目录安装时才取 plugin-catalog/ 平台的目录条目名、其他一切插件平台为 plugin、其他一切表面为 none)。终端计数器(hermes.task_run.finished)含起始字段,加上有界的结果、结束原因、终止状态,以及失败任务的 failure_class:当轮次死于已分类的 API 错误时为提供商 FailoverReason,否则为本地类别(empty_response、context_compression、repeated_errors、exception、other……)。同一结束事件还向 hermes.task_run.duration 送入执行表面、结果、时长桶和提供商重试次数桶。软件包 v2 把时长、重试和按任务的模型/工具调用计数放在终端行本身,使几乎每个任务都自成一行;每轮次的调用计数位于 hermes.task_cost.count。原始退出原因永不出机器。重试是同一 Hermes API 请求 id 的额外提供商尝试;它们不抬高逻辑模型调用计数。工具调用在观察到终端工具结果后,按其 Hermes 工具调用 id 去重。外层 AIAgent 执行边界在正常返回、提前返回、异常和取消时关闭任务。若 Hermes 在上下文压缩期间轮换其对话会话,活动任务归属跟随任务 id。
每次工具调用由一个名为 hermes.tool_call 的 Relay 工具生命周期表示。终端计数器只含有界的工具类别、结果和审批结果;同一事件还向 hermes.tool_call.latency 送入工具类别、延迟桶和显式重试次数桶(软件包 v2 把延迟和重试放在终端行,每几次调用一行)。Hermes 从运行时注册表中已声明的工具集推导类别;自定义和未识别的工具集归并为 other,而非导出工具或插件名。同一终端事件还向 hermes.tool.usage.count 送入 tool_name、outcome 和 error_class。tool_name 仅对仓库静态 toolsets.TOOLSETS 中声明的工具(toolsets.BUILTIN_TOOL_NAMES,在任何运行时自定义工具集创建之前捕获)导出;MCP 工具报告 mcp,每个插件或自定义工具报告 plugin。error_class 映射 Hermes 自己的 error_type 值(tool_error、timeout、interrupted、invalid_arguments、blocked、contract_violation);任何其他值(如异常类名)归并为 exception。Hermes 不从重复工具名或相邻调用推断重试;当钩子未提供显式重试关系时,重试桶为 unknown。审批决策以 hermes.tool_approval mark 发出,并记录为归属于某个工具调用或显式 unattributed。非内建工具名、调用 id、参数、结果、命令、描述和错误文本不包含在共享指标事件或软件包中。一个已开始、在其任务终止时仍处于打开状态的工具,会以 failed、timed out 或 cancelled 关闭,并计入该任务的工具计数桶。
成功的 skill 变更发出 hermes.skill.lifecycle mark,只带有界的动作和来源。成功的加载发出 hermes.skill.load mark,带有界的来源、首次使用或复用状态、补丁后复用状态、使用次数桶和 skill_name:仅当该 skill 是 Hermes 发布的 skill(skills/ 或 optional-skills/)时才是 skill 名,否则为 custom。Hermes 在其既有 skills/.usage.json 状态中以事务方式推导复用和补丁世代连续性;本地或智能体创建的 skill 名以及精确计数或世代绝不进入 Relay 指标事件、SQLite 维度或软件包。新补丁后的一次使用计为一次 reused_after_patch;此后的使用仍是普通复用,直到下一次补丁。补丁后的任务结果归属在其窗口和多 skill 语义被定义前仍推迟。
每滚动 24 小时一次,首次激活还发出一个 hermes.install.snapshot mark,描述该 profile 如何配置:记忆提供商(一个内置提供商名、builtin 或 plugin)、MCP 服务器数、已启用插件数、已安装 skill 数、已启用 cron 作业数、profile 数和已连接消息平台数的分桶计数、主提供商 id、终端后端(local、docker、ssh……或 other)、显示语言(一个发布的 locale 或 other)以及 install_age_bucket:该 profile 有史以来第一个会话开始距现在多久。安装时长让后端能区分新用户和刚选择加入的老用户。服务器、插件、skill、作业和 profile 名绝不读入事件。与 hermes.client.active 相同的 compare-and-set 闩锁让它每天每安装至多一行,生产者在遍历 skill 树之前先检查闩锁。
该快照还携带六个版本滞后、渠道和硬件字段,全部离线读取(无网络调用、无子进程):
release_channel(stable、main、dev、unknown):打包构建的固化渠道(main 的 canary 构建读main)、源码安装的渠道记录,否则为 checkout 的分支(main/master 为main,其他分支为dev)。git 远程 URL 和分支名绝不读入事件。version_age_bucket(lt_7d……gte_90d、unknown):已安装版本的年龄,取自安装戳或 checkout 中它自己的提交日期。behind_bucket(0、1、2、3_to_5、6_to_10、gte_11、unknown):落后多少个提交或版本,仅来自更新检查对该精确修订的缓存结果且未满 7 天;否则为unknown。ram_bucket(lt_8g……gte_128g、unknown):已安装内存按标称容量取整(操作系统总量按 1.1 缩放以预留固件)。gpu_class(nvidia、amd、intel、apple_silicon、none、unknown):Linux 上来自/proc/driver/nvidia或 DRM PCI 厂商 id 的最高优先级 GPU 厂商,Windows 上来自显示适配器注册表类,macOS 上原生 arm64。绝不是型号名、驱动版本或 VRAM 大小。local_model_provider_used(yes/no):主模型或任何辅助任务是否运行在本地或自托管服务器上(一个本地提供商 id,如 Ollama/LM Studio/llama.cpp,或一个环回/私网 base URL)。URL 本身保留在本地。
决策数据指标 {#decision-data-metrics}
这些回答活动计数器无法回答的产品问题:什么让人留下、新用户在哪里流失、哪些表面和模型承载真实使用、哪些扩展值得投入。每个维度都是封闭枚举、桶、提供商/模型标识符(如模型路由上那样)或 Nous 自己发布的公开名。
| 指标 | 维度 | 它回答的问题 |
|---|---|---|
hermes.session.count | 入口、表面、平台、轮次/失败轮次桶、活跃时长桶、最终结果、消息/模型调用/工具调用计数桶(0 …… 101_to_250、251_to_1000、gte_1001) | 每个表面的真实使用有多深;会话是否在失败后立即结束?每个对话一行,在表面关闭它时写入:压缩轮换交出的会话 id 会合并成一行(被退役的 id 在其在飞轮次结束后即关闭);网关重置、空闲过期、/new 或 /branch 开启新对话,即使它把旧会话记为父级。复用会话 id 的后台审查分叉不增加轮次、调用或消息。消息 = 用户轮次 + 主模型回复 + 工具结果;模型调用 = 逻辑主 API 请求(不含重试)。 |
hermes.install.milestone | milestone、安装时长桶 | 从安装到首次成功、首个网关消息、首次 cron 运行、首次委派、首次创建 skill(由 Hermes 后台审查创建的那次不计)、首次长会话分别要多久?每安装记录一次。 |
hermes.setup.completed | 表面(cli/desktop)、提供商 | 人们在 setup 时选哪些提供商,在哪个表面。 |
hermes.model_tokens.sum | 调用角色、模型、提供商、辅助任务、token 类型 | 每个模型/提供商的 token 量、提示词缓存占比,以及哪些辅助工作(压缩、标题、视觉……)耗费多少。值是 token 总和,不是事件计数。 |
hermes.model_route.count 的 ttft_bucket | 首 token 时间 | 每个提供商/模型的感知延迟。 |
hermes.compression.count | 触发、结果、上下文填充桶 | 压缩多久运行一次、上下文填到多满、是否失败。 |
hermes.model_switch.count | 来自/去往提供商、表面 | 人们离开哪些提供商、转向哪些。 |
hermes.fallback.count | 来自/去往提供商、错误类别 | 回退提供商多久挽救一轮次,以及从什么错误挽救。 |
hermes.slash_command.count | 命令、表面 | 哪些内建命令被使用(/retry、/undo、/new 是摩擦信号)。skill 和插件命令报告 skill/plugin。 |
hermes.extension.install.count | 种类、来源、名、结果 | 哪些目录 skill、MCP 服务器和插件被安装。name 是内置/可选 skill、optional-mcps/ 或 plugin-catalog/ 条目,否则为 custom。 |
hermes.memory.op.count | op(add/replace/remove/read/search/other)、提供商(builtin、一个内置记忆插件,否则 plugin)、来源(foreground/background_review)、结果(success/failed/rejected) | 学习循环是否在写记忆、谁请求它(用户轮次还是后台审查),写入被拒或失败多频繁。绝不包含记忆文本。 |
hermes.curator.run.count | 触发(scheduled/manual)、结果(success/failed/skipped)、归档/合并/补丁/创建桶 | skill 策展器是否运行,是否真的整合了任何东西。试运行报告 skipped;一次计划检查若发现另一个进程已在跑该轮,则什么都不记录。绝不包含 skill 名。 |
hermes.delegation.run.count | 子智能体计数桶、深度(1–3、gte_4)、模式(foreground/background)、结果(success/partial/failed/cancelled) | delegate_task 扇出多宽多深,每个子任务完成多频繁。每次调用一行,无论它拆成多少完成单元。 |
hermes.execution_backend.count | 种类(terminal/browser/code)、后端、结果、错误类别 | 哪些沙箱承载真实工作,各自多可靠。终端后端是 terminal.backend 的值(否则 other);浏览器后端是 local、lightpanda、cdp、camofox、extension 或一个内置云提供商(否则 other);execute_code 为 local 或 remote。一条命令自身的非零退出仍是后端成功,但前台命令超时为 failed/timeout;终端和 execute_code 调用在到达后端前被守卫拒绝、Hermes 自己为 TUI/Desktop 路径补全做的列目录、以及后台审查和策展器分叉发出的调用不计入。 |
hermes.platform.health | 平台、事件(connect_ok/connect_failed/reconnect/disconnect)、错误类别(auth/network/rate_limited/config/other) | 哪些消息平台连接失败或掉线,原因是什么。从异常类型、HTTP 状态和 Hermes 自己的致命码分类,绝不含错误文本。 |
hermes.platform.delivery | 平台、结果(sent/failed)、失败类别(rate_limited/too_long/auth/network/forbidden/other) | 每个平台回复多久送不到用户(每条逻辑回复计一次,含重试)。 |
hermes.gateway.reply_latency | 平台、首响应桶(lt_2s …… gte_60s) | 从一条入站消息被接收到首个可见回复文本的时间(流式首块或最终消息)。 |
hermes.cron.run | 结果(success/failed/missed/skipped)、投递种类(local/platform/webhook/none/other)、时长桶 | 计划作业是否运行、失败、被门槛或重叠跳过,或在 Hermes 宕机期间错过。作业名、提示词、计划和目标绝不包含。 |
hermes.startup.latency | 表面(cli、tui、desktop_attach、gateway_boot、serve_boot)、延迟桶(lt_500ms …… gte_10s) | 每个表面从启动到可用要多久,以便启动回归按表面和发布显示。每次进程启动一行:CLI = 进程启动到首个渲染提示(或派发一条 -q 查询;看板 worker 除外),TUI = Ink 进程启动到网关就绪,Desktop = 应用启动到后端挂接,gateway = 进程启动到适配器已连接,hermes serve = 进程启动到监听。不计入:原地 re-exec 的进程(如 hermes sessions browse 恢复会话)和每个 dashboard Chat 标签终端;TUI/Desktop 重连同一后端不重复计数。 |
hermes.update.run | 种类、结果、失败阶段、时长桶、来源版本年龄桶、应用模式 | 更新是否成功、耗时多久、在哪里失败,以及被更新的版本有多旧。hermes update 行来自最终更新回执,每次运行一行(当 Desktop 的源码 checkout 交接运行它时,kind 为 desktop);更新前解释器跑完的一次运行在本地暂存,只带这些字段,且仅在采集开启时,由下一次启动计数;Desktop 打包自更新(apply_mode=package)由应用在应用它们的重启后报告一次。 |
hermes.update.stage | 阶段、结果、时长桶 | hermes update 各阶段(plan、snapshot、apply、deps、build、restart、verify)的结果和墙钟时间,取自回执的阶段时间戳。 |
hermes.process.exit | 进程种类、退出种类、崩溃类别 | CLI / TUI / gateway / serve / cron-tick 进程如何结束(clean、仅带异常族的 crash、killed、watchdog),由同 profile 的下一次启动从本地标记报告(采集关闭的启动会删除这些标记以及待处理的 provider_setup 标记,不予报告)。被轮次看门狗中止的轮次也计为 exit_kind=watchdog。 |
中继连接器承载的回复报告对话所在的平台(入站的 platform,否则当连接器恰好只代理一个平台时为其所代理的平台),绝不报告 relay;只有两者都未知时才保留 relay。一条入站未被连接器盖戳的轮次仍是网关消息(execution_surface=gateway,任务/会话 platform=relay)。中继连接器的 hermes.platform.health 保持 relay:它的一条 socket 代理多个平台,因此一次连接或掉线不属于其中任何一个。
桌面应用:什么被使用、什么碍事、什么被关掉
由桌面应用记录到聚焦 profile 的存储中,且仅在该 profile 的采集开关开启时。关闭时应用不留本地记录(关闭开关会删除已保留内容),也不发送任何内容。应用按每个网关连接和 profile 保留一条本地记录,由该 profile 的窗口共享;切换 profile 后,在新 profile 的开关被读取前不保留也不发送任何内容,且每当窗口重新获得焦点时重新读取开关(因此从 CLI 或另一个窗口退出选择会在那里生效)。没有评分弹窗或其他新 UI;每条事实都来自应用已有的一次交互。每个值都是应用代码中定义的封闭 id(区域、动作、通知、流程、开关、步骤)、一个已发布配置键或一个桶。消息文本、toast 文本、会话/bot/profile 名、路径和设置值绝不出站。
| 指标 | 维度 | 它回答的问题 |
|---|---|---|
hermes.desktop.feature_use | 区域(面板、命令面板、模型/会话选择器、语音、Bot Mode、皮肤、项目、每个设置页、整页、other) | 哪些桌面区域根本被使用,每个区域每个 UTC 天每个 profile 至多计一次(闩锁在 profile 本地数据库,因此第二个窗口或后端重启绝不重复计数)。 |
hermes.desktop.action_use | 动作(应用内建命令/快捷键 id 加少数命名按钮;插件命令和数字槽位快捷键为 other,在保留任何内容前于应用内归并)、方式(click/shortcut/palette/menu)、计数桶 | 人们按哪些按钮和命令,以及如何按:在应用内按 profile 聚合并每个完成的天报告一次(无逐次按下行)。该行落在它所描述那天的时段,而非发送那天;每天每 profile 记录一次,无论多少窗口或后端重启报告它,超过 8 天的天被丢弃。 |
hermes.desktop.mode_use | 模式(sessions/bots)、活跃分钟桶、发送消息桶、bot 计数桶 | 桌面时间在 Bot Mode 和普通 Sessions 之间如何分配。当天使用的每个模式一行,落在当天时段(与 action_use 相同的每天每 profile 闩锁);活跃时间为相邻交互间隔之和,每个间隔上限 5 分钟。 |
hermes.desktop.friction | 种类(notice_dismissed、error_toast、renderer_crash、backend_disconnect、slow_frame)、细节(通知 id、错误类别、崩溃原因、掉线原因、帧时长桶) | 什么碍事。错误 toast 只携带其代码定义的类别;渲染器崩溃仅在崩溃窗口自己的 profile 采集时由应用外壳记录,并在该 profile 的一个窗口恢复后报告;慢帧是窗口可见时的长帧,每天有上限。 |
hermes.desktop.dislike | 信号(quick_close、cancelled、setting_off_default、rage_click、undo、feature_disabled)、目标、设置、方向 | 功能不受欢迎的信号:面板在打开后 5 秒内关闭、对话框/流程中途退出、设置被移到或移离默认值(仅键;后端自己把保存值与默认比较)、一秒内对同一控件三次点击、一次撤销、一个发布的功能被关闭。按信号按天上限。 |
hermes.desktop.onboarding | 步骤(首次运行步骤:提供商选择器、登录、API key、本地端点、模型选择、稍后选择、免费层界面、引导设置卡片、同意、首条消息)、事件(reached/completed/abandoned) | 首次运行停在哪里。每个步骤事件每个 profile 一次(闩锁在一个小的按 profile 文件里);abandoned 是应用下次启动时仍打开的步骤。首次运行发生在同意问题之前,因此在回答之前应用只把步骤事件留在内存中(绝不在磁盘、绝不发送),并在用户于该应用会话中选择加入时记录;回答"no"或退出则先丢弃它们。在桌面关闭采集会连同应用自己的副本删除这些闩锁。 |
会话在关闭时(finalize、reset 或进程退出)被汇总;被委派的子会话不单独计数。milestone 闩锁在本地数据库,因此每个无论多少进程到达都只触发一次。
按模型的质量、摩擦与上下文压力 {#per-model-quality-friction-and-context-pressure}
提供商和模型遵循模型路由规则:Hermes 发布的提供商(内建、树内 plugins/model-providers/ 的 profile 或公开 models.dev id)及其模型 id;自定义端点、安装在 $HERMES_HOME/plugins/model-providers/ 下或来自 pip 的提供商插件(含名称和别名)、custom 的本地服务器别名(ollama、local、vllm、llamacpp、llama-cpp、llama.cpp)以及环回服务器读 custom。一个发布的提供商若其端点是环回服务器(lmstudio,无论哪个别名)保留其名,但其模型读 custom。一个模型若其提供商未知,或其 id 是 URL、文件路径或网络地址(host:port、IP 地址、localhost)或 AWS ARN(它携带账户 id),读 custom。在 Azure 提供商上,模型 id 是其属主选择的部署名,因此仅当它是公开模型 id(Hermes 的模型目录或本地 models.dev 缓存,如 gpt-4o)时通过;acme-legal-prod 读 custom。本地订阅器对每个 mark 的提供商/模型字段重新运行这些规则,并丢弃它们会改写的行。
| 指标 | 维度 | 它回答的问题 |
|---|---|---|
hermes.model_tool_quality.count | 提供商、模型、调用角色、问题(none、invalid_json、unknown_tool、schema_mismatch、empty_arguments、repaired) | 哪些模型发出损坏的工具调用,Hermes 多久得修复它们。每个发出的调用计一次(干净的为 none),因此该值是比率分母。empty_arguments 只对带必填参数的工具计数;repaired 表示 Hermes 修正了工具名或参数 JSON 并运行了调用。 |
hermes.model_friction.count | 提供商、模型、信号(retry、undo、interrupt、quick_abandon、switch_away) | 用户在和哪些模型较劲。归属于产出该轮次的模型:/retry 和 /undo 在其执行处、用户对交互轮次的中断、失败轮次后 60 秒内结束的会话、以及 /model 切离该模型。 |
hermes.context_peak.count | 提供商、模型、峰值填充桶、窗口桶(lt_32k …… gte_1m)、是否触顶(yes/no) | 会话离每个模型的上下文窗口多近,多久溢出一次。每个关闭对话一行:压缩轮换交出的会话 id 报告一次,取最满的段;limit_hit 表示一次主调用因过大被拒(上下文溢出或 HTTP 413),即 Hermes 以强制压缩应对的那些拒绝。 |
智能体框架准确性 {#agent-harness-accuracy}
这些调优智能体循环本身。Hermes 自己的后台审查和策展循环绝不计数;被委派的子智能体则计数(它们的工具调用、循环和回复也是模型行为)。命令文本、文件路径、工具参数和回复文本绝不出站——只有下列封闭值。
| 指标 | 维度 | 它回答的问题 |
|---|---|---|
hermes.file_edit.count | 工具(patch、write_file)、模式(replace、v4a、whole_file)、结果(applied、already_applied、no_match、ambiguous、failed)、匹配策略(patch 工具的模糊匹配链:exact、line_trimmed、whitespace_normalized、indentation_flexible、escape_normalized、trimmed_boundary、unicode_normalized、block_anchor、context_aware;什么都没匹配时为 none) | 哪些模糊匹配策略物有所值,编辑多久落空或产生歧义。每次编辑工具调用一行;多 hunk V4A patch 报告任一 hunk 所需的最宽松策略。 |
hermes.loop_guard.count | 提供商、模型、信号(repeated_tool_call、loop_detected、iteration_cap)、检测器(exact_failure、idempotent_no_progress、same_tool_failure、identical_call_streak、identical_cycle、web_search_cap、subagent_cap、iteration_budget) | 每个卡死循环守卫按模型多久触发一次。repeated_tool_call 是带警告仍运行的调用,loop_detected 是阻断或停止,iteration_cap 是耗尽迭代预算的轮次。每个轮次每个信号和检测器至多一次。 |
hermes.tool_recovery.count | 提供商、模型、工具(内建名,否则 mcp / plugin)、下一工具(same、different、none)、下一结果(success、error、no_tool_call、gave_up) | 模型在工具调用失败后是否恢复。每次失败调用一行,对照模型下一轮解决:它对同一工具的下一次调用,否则它的第一次调用;它改用文本回答时为 no_tool_call,轮次在其回复前结束(停止、预算耗尽、中断、出错)时为 gave_up。 |
hermes.terminal.outcome.count | 后端(终端后端)、命令种类(git、package_manager、build、test_runner、python、node、shell_builtin、shell、file_ops、network、container、other)、结果(ok、nonzero、timeout、killed) | 哪些种类的命令按后端失败或超时。种类来自首程序词(环境赋值和 sudo 式包装之后)的一张固定表。每条到达退出状态的前台命令一行;timeout / killed 来自 Hermes 自己的死线和中断标志,因此命令自己的 exit 124 是 nonzero。hermes.execution_backend.count 按后端是否服务了它们来数同一批调用——维度不相交,不是对结果的第二次计数。 |
hermes.model_reply_issue.count | 提供商、模型、问题(none、empty、reasoning_only、refusal、truncated_length) | 哪些模型返回不可用回复。每个主模型响应一行(可用的为 none,即比率分母)。refusal 和 truncated_length 仅来自结构化结束原因(content_filter、length);empty 是没有可见文本、工具调用或推理的有效响应。 |
效率:轮次成本、浪费、工具开销与提示词缓存失效 {#efficiency-turn-cost-waste-tool-overhead-and-prompt-cache-breaks}
提供商和模型遵循上文的模型路由规则。一个"用户轮次"是一条用户消息到其最终回复;Hermes 自有的工作(后台记忆/skill 审查、策展器、被委派子智能体自己的轮次)不是用户轮次。cache_break 和 tool_output_truncation 描述模型和工具行为,因此被委派的子智能体计入那里;后台审查和策展器永不计入。
| 指标 | 维度 | 它回答的问题 |
|---|---|---|
hermes.task_cost.count | 提供商、模型、token 桶(lt_2k …… gte_1m、unknown)、工具调用桶、API 调用桶(0 …… 51_to_100、gte_101)、结果(completed、interrupted、failed) | 用户轮次按模型耗费多少。token = 提示词(含缓存读/写)加该轮次主调用的补全;提供商未报告用量时为 unknown。用户看到结束的每个交互轮次一行(会话关闭中止不算轮次)。 |
hermes.wasted_tokens.count | 提供商、模型、原因(interrupt、retry、undo)、token 桶 | 用户扔掉多少 token。每次被中断、/retry 或 /undo 丢弃的轮次一行(/undo N 计 N 轮),归属于产出该轮次的模型;先中断后撤销的轮次计一次。本进程没看到该轮次时(重启、远程主机)为 unknown。 |
hermes.tool_output_truncation.count | 工具(发布的工具名,否则 mcp / plugin)、是否截断(yes/no)、原始大小桶(字符:lt_1k …… gte_500k) | 哪些工具产出的输出太大而无法内联保留。每个工具结果一行;工具自己截断输出时(终端、execute_code 和 MCP 头/尾截断;此时大小是原始大小),或按结果上限或按轮次预算把它溢出到磁盘时为 yes。 |
hermes.tool_overhead.count | 已启用工具数桶、工具 schema token 桶(0、lt_2k …… gte_40k)、执行表面 | 携带工具定义耗费多少。每个关闭的交互对话一行:它启用的工具,以及 Hermes 自己估计的这些工具定义给每次请求增加的 token。 |
hermes.tool_enabled_unused.count | 工具集(Hermes 发布的工具集;MCP 服务器、插件和用户工具集读 custom)、是否使用(yes/no) | 哪些默认工具集被付费却从不使用。每个关闭交互对话中每个已启用工具集一行(以发布的工具集为界)。 |
hermes.cache_break.count | 提供商、模型、原因(compression、model_switch、toolset_change、system_prompt_rebuild、provider_reported_miss、cache_expired) | Hermes 多久扔掉一次热提示词缓存,以及原因。compression 是预期的;model_switch、toolset_change(会话中途工具数组变化)和 system_prompt_rebuild(一个持续对话重建了系统提示词而非回放存储的字节)是 Hermes 已知原因;provider_reported_miss 是一次主调用在同模型一次热读之后读到零缓存 token 且无 Hermes 已知原因,cache_expired 是空闲至少五分钟后同样如此。已知原因不会再作为 miss 重复计数。 |
参与度与隐式模型满意度 {#engagement-and-implicit-model-satisfaction}
| 指标 | 维度 | 它回答的问题 |
|---|---|---|
hermes.engagement.surface_day.count | 表面(cli、tui、desktop、gateway、acp)、活跃分钟桶(0、lt_5m、5m_to_30m、30m_to_2h、2h_to_6h、gte_6h) | 每个表面每天实际使用多久。一个关闭的 UTC 天里使用的每个表面一行。 |
hermes.engagement.day.count | 活跃分钟桶、使用表面数(0–3、gte_4)、主提供商、主模型、活跃 profile 计数桶 | 每周活跃天数、多表面使用,以及按模型的次日/次周回访。一个人使用 Hermes 的每个关闭 UTC 天一行。根(默认)profile 在只有其他 profile 活跃的天也写一行主机行:surfaces_used_count 为 0、活跃分钟 0,携带活跃 profile 计数;统计活跃天数时排除 surfaces_used_count=0 的行。 |
hermes.model_switch_after.count | 提供商、模型(被切离的模型)、切换前轮次桶(1、2_to_3、4_to_10、11_to_30、gte_31) | 用户在 /model 离开一个模型之前在它上面待多久。统计对话中在旧模型上发出的用户轮次(含压缩段;故障转移到回退的轮次仍计入它被发出时所在的模型;后台审查分叉不是轮次);在当前模型上任何轮次之前切换不计。 |
活跃时间按 UTC 天在本地累积:相邻交互间隔之和(交互 = 用户轮次在交互表面或网关消息上开始或结束;无人值守的 cron 运行——由 hermes.cron.run 计数——被委派子任务、后台审查、策展器、批处理和 API-server / python 嵌入除外),每个间隔上限 5 分钟。当天的行在当天关闭后、由次日的第一次交互、在一个数据库事务中记录,因此无论多少进程看到跨日,每天每 profile 恰好报告一次;它们的日期记在所描述的那天。主模型是当天服务了最多那些用户轮次的模型(都没有时为 none),按模型路由规则命名。每周活跃天数和按模型回访由服务端从这些日行和既有 install_id 推导:Hermes 不为它们保留周窗口,也不保留 install_id 之外的标识符。
active_profile_count_bucket 统计该 UTC 天内有用户自有轮次(上述交互)的、主机上不同的 profile 数。每个 profile 把它的轮次折进一个保存在根(默认)profile 数据库里的主机累加器,作为各 profile home 的不透明本地哈希,永不出站,因此一个 profile 无论由哪个进程或多路复用运行时服务都只计一次。只有根 profile 的日行携带该计数(即使根自己那天空闲,它也报告一天,活跃分钟和表面为 0);其他每个 profile 的行读 0。当根 profile 采集关闭时,不写它的数据库,也不报告该计数。
上手引导与功能信号 {#onboarding-and-feature-signals}
| 指标 | 维度 | 它回答的问题 |
|---|---|---|
hermes.tool_unavailable.count | 提供商、模型、工具名(仅发布的内建) | 哪些工具集应默认开启:模型调用了一个 Hermes 发布、但本会话未启用的工具。任何其他未知名(插件、MCP、幻觉出来的)只作为 model_tool_quality 的 unknown_tool 问题保留。会话启用了但被 tool_search 延后(仍可经 tool_call 到达)的内建工具不算不可用。后台审查、被委派子任务和 cron 作业的工具集是刻意收窄的,不计入。 |
hermes.provider_setup.count | 提供商(目录名;自定义端点读 custom)、表面(cli_setup、cli_model、tui、desktop、dashboard)、事件(started、completed、failed、abandoned)、失败类别(auth、network、no_models、cancelled、other;除非失败否则为 none) | 连接提供商在哪里卡住。started 在选定提供商后计一次;流程的结束由运行它的表面记录。在 CLI 选择器中,Esc 以 failed/cancelled 结束流程;Back(左箭头)保持打开,因此再次选同一提供商会继续它(一次 started),而选另一个提供商或离开命令则以 cancelled 结束。没人完成的流程留下一个本地标记,由下一次 setup 开始或该 profile 的 Hermes 启动报告为 abandoned(其进程已消失,或它已挂起超过一小时);留到过期的 OAuth device code 也是 abandoned。从表单(TUI/Desktop/dashboard)保存的新的或变更的提供商 API key、以及新增的自定义端点,在一次动作中开始并完成;清除 key、重新保存同一 key、编辑既有端点、生态 token(GITHUB_TOKEN、GH_TOKEN、HF_TOKEN)以及工具设置面板也会索要的 key(如 GEMINI_API_KEY、XAI_API_KEY、DEEPINFRA_API_KEY)不从通用 key 表单计数(Desktop 的上手引导和模型设置把它们的保存标记为提供商连接,因此那些计数)。绝不含 key、token、base URL 或错误文本。在选定之前离开提供商选择器不计。 |
hermes.feature_adoption.count | 功能(memory、skills_created、delegation、cron、gateway_platform、desktop、tui、mcp、plugins、browser、voice、kanban、projects、bot_mode、curator)、安装后天数(same_day、1d_to_7d、7d_to_30d、30d_to_90d、gte_90d、unknown) | 每个主要功能在安装后多久首次被真正使用。每功能每安装一次,闩锁在本地数据库,由上述计数器推导(一次前台记忆写入、一个应用户要求创建的 skill(非 Hermes 后台审查创建)、一次成功的 MCP/插件/浏览器/TTS/看板工具调用、一次 Desktop/TUI/网关任务、一次 cron 运行、一次手动策展器运行;计划的策展轮次不计),外加 Bot Mode 消息和项目创建的直接首次使用报告。年龄取所属 profile 的(其首个会话)。 |
hermes.feature_disabled.count | 种类(toolset、skill、plugin、platform、setting、memory、curator、compression)、名、表面(cli_tools、cli_config、cli_slash、tui、desktop、dashboard)、事件(disabled、re_enabled) | 用户关掉什么。在配置写入本身做 diff:一个默认开启的工具集被移除、一个 skill 或插件被加入其禁用列表、一个默认 true 的设置被设为 false(以及每次移回)。名仅在发布时公开——工具集键、内置/目录 skill、内置/目录插件(消息平台插件报告为 platform)、DEFAULT_CONFIG 键路径(绝不是值)——否则为 custom。卸载目录 skill 计为 disabled。只有用户入口点记录(hermes tools / config / skills / plugins、聊天斜杠命令、TUI/Desktop、dashboard);setup 和迁移不记录,即使迁移在它们之一内部运行(hermes config migrate、从 dashboard 创建的 profile)。值为 ${VAR} 模板的设置不比较。diff 和记录在写入后的后台线程运行,在每个配置锁之外。每天每 (kind, name, event) 至多一次。 |
本地状态写入于:
$HERMES_HOME/telemetry/shared_metrics/metrics.sqlite3
$HERMES_HOME/telemetry/shared_metrics/outbox/*.json
数据库保存事务性聚合和包发件箱状态。包文件是不可变的 delta 文档,符合封闭 JSON schema,并以原子替换写成紧凑 JSON(jq . 可美化一个)。一旦 ingest 接受或拒绝一个包,数据库只保留其发送状态并丢弃它那份正文副本;文件是本地历史副本。每个包把 Hermes 版本、操作系统族、架构和安装方式记录为有界客户端资源。无法识别的平台或安装值导出为 unknown;原始平台字符串、主机名和路径绝不包含。完全打包的聚合行、成功导出的包行和文件在本地保留 30 天。待处理的包行和带未导出 delta 的计数器永不清理。软件包 schema v1 和 v2 对既有 outbox 文件保持不变。新包使用 v3,它也接受 hermes.model_route.count、hermes.tool_call.count 和任务计数器的 v2 字段集,以便升级前记录的计数器安全排空。从仓库内注册表派生的词表(工具名、平台、记忆提供商、错误类别)在 JSON schema 中按模式有界;权威白名单是 shared_metrics_contract.py。
每个包包含一个以随机 UUID 生成的 install_id。尽管有该 schema 字段名,它当前的范围是一个 HERMES_HOME,因此更准确地说是一个持久的假名化 profile 标识符。它不从硬件、账户、主机、路径或凭据数据派生。它在来自该 profile 的各包间保持稳定,因此可以链接这些本地包。删除 $HERMES_HOME/telemetry/shared_metrics 会连同所有聚合和包文件一起重置该标识符。
远程投递是可选加入,默认关闭。要在远程复用这个持久的本地标识符,需要单独的产品和隐私决策,覆盖同意、身份范围、重置行为、保留和删除——该决策已经做出。
这些决策记录在 附录 A,实现它们的导出器已发布。仅采集仍不传输任何内容:发送器仅在
telemetry.shared_metrics.send也为 true 时运行。每个传输的包原样携带稳定的install_id(产品决策,2026-08-27——含已被取代的 HMAC 假名设计,记录见 A.2)。
安装身份的范围是一个 HERMES_HOME。要重置它,停止 Hermes 进程并删除 $HERMES_HOME/telemetry/shared_metrics。这刻意同时移除旧身份、聚合数据库和排队的本地包;下一个获得同意的会话创建新身份。关闭共享指标会停止新的采集,但不会静默删除此前采集的本地状态。
冒烟测试 {#smoke-test}
对确定性本地模型服务器运行一次真实的 Hermes CLI 轮次:
./.venv/bin/python scripts/smoke_nemo_relay_shared_metrics.py
脚本默认使用已安装的 nemo-relay 依赖。仅在测试本地构建的 Relay 绑定时传 --relay-python ../nemo-relay/python。
冒烟测试让本地模型在最终响应前发起一次真实的 read_file 工具调用,然后通过已安装的 Relay 绑定驱动 create、load、reuse、patch、edit、stale、archive、restore 和 install skill 转换。它验证 SQLite 中的模型、提供商、任务、工具和 skill 计数器,按封闭 schema 校验所有导出的 delta 包,验证假名化的 client-active 计数器,并检查提示词、响应、工具调用 id、工具结果和 skill 名这些金丝雀标记在包中缺席。
附录 A:远程导出器决策(第 2 阶段){#appendix-a-remote-exporter-decisions-phase-2}
状态:已实现。 本附录回答"当前切片"留给未来远程导出器的产品和隐私问题。它记录决定了什么以及为什么,让推理在实现之后仍可追溯。
发送默认关闭,且需要 telemetry.shared_metrics.enabled 和 telemetry.shared_metrics.send 同时为 true。
导出器把已写入 $HERMES_HOME/telemetry/shared_metrics/outbox/ 的包文件发送到 Hermes 遥测 ingest 服务。该服务只校验信封(schema_version 加一个 UUID package_id),并把正文逐字存入 S3。
A.1 同意 {#a1-consent}
传输是与采集分开的可选加入,通过一个新配置键:
telemetry:
shared_metrics:
enabled: false # 本地采集
send: false # 新增:传输到 Nous 遥测服务
send默认为 false。仅采集绝不传输。send要求enabled。它不蕴含enabled:一个传输开关不应静默开启采集。send: true配enabled: false会警告且什么都不做。- 与
enabled一样,send归 profile 所有,不被托管范围配置覆盖。
两个键都每个 profile 只问一次:由 hermes setup 的 Shared Metrics 小节,或在 Hermes Desktop 中由输入框上方的一条提供条(Send to Nous / Local only / No thanks,附 Details 视图)。Desktop 提供条绝不遮挡输入框或抢占焦点,仅在首次运行上手引导后出现,并保留到被回答。其 config.yaml 已携带任一键的 profile,在任何表面都不再被问。Settings › Safety › Privacy & network 稍后可切换这两个键。
一个包只有在其整个时段都落在已记录的同意窗口内才被发送。 同意以显式区间存储在共享指标 SQLite 存储(send_consent_windows):当首次观察到 send: true 时窗口开启,此后每次观察都向前确认,在观察到 send: false 时关闭——关闭在最后一个已确认时刻,而非墙上时钟。单个 reconciler 在每次进程启动时从配置推导这张表,因此向导变更、手改 config.yaml 和中途吊销都走同一路径,任何一个都不会漏掉某次过渡。
任何时段早于第一个窗口、落在窗口之间、或越过最新确认时刻的包都被排除——门槛失败即关闭。因此一个新包在其时段完成后,至多等一次进程启动才有资格被发送。
门槛设在时段上,而非包的创建时间。一个时段可能被拆成若干在不同天创建的包:某天的第一个包当天写入,同一时段的一个尾部包通常次日跟随。按创建时间设门槛会发送一个时段的尾部却丢弃其头部,报告一个被静默少计的天。按时段设门槛让同意只向前推进,且每个被传输的时段完整。
本地历史最长可达 30 天,而那些数据是在"不上传任何东西"的承诺下采集的。只向前兑现同意,代价至多是我们从未获准发送的 30 天积压。
A.2 身份范围——稳定 install_id 原样传输 {#a2-identity-scope--the-stable-installid-is-transmitted-as-is}
决策记录。 本导出器的原始设计(以及本附录的第 1–8 版修订)传输的是一个带密钥的假名,而非该标识符:HMAC-SHA256(key = 本地持有的轮换 salt, message = install_id),salt 每 30 天轮换。在 2026-08-27、功能发布之前(零个已同意用户、零次生产传输),产品负责人判定分析所需的是一个稳定的跨窗口身份——留存曲线、纵向安装行为——而轮换在设计上会摧毁它。假名化层被整体移除,而非就地削弱。
现在传输的是:
- 每个包逐字携带
install_id:即上文所述持久的、按 profile 的随机 UUID。 - 它在本地生成(
uuid4),不含硬件、账户、用户或机器派生信息,标识的是一个 profile,而非一个人。 - 它保持稳定,直到用户删除共享指标目录,那会重新生成它(见 A.4)。
后果被如实陈述,而非掩盖:
- 来自一个 profile 的包无限期相关联,而非按窗口。一次安装的每日信封序列的长期可链接性现在是设计行为,而非残留。
- 旧设计的 A.3 残留分析(稳定的
resource元组 + 跨轮换窗口桥接的连续时段)已无意义——不再有窗口边界可桥接。 - 设置向导的同意文案明确陈述了这个身份模型;它在移除派生的同一变更中被更新,因此在任何已发布构建中都不曾按旧文案采集过同意。
逐字节相同的重发仍然成立。 传输的 id 在包首次准备时记录在行上(sent_install_id),线上正文总是从该记录值重建,因此重试重建出相同字节。契约要求这一点:用不同内容重发一个 package_id 是未定义行为。(有了稳定 id,记录副本不再承担对抗轮换的作用——它作为审计列保留,并作为抵御未来任何身份语义变更的廉价保险。)
A.3 轮换——已移除(决策记录){#a3-rotation--removed-decision-record}
salt 轮换随派生一起被删除(产品决策,2026-08-27)。本节保留作为早先设计做了什么、以及为何接受移除的记录:
- 轮换的存在是为了限制长期可链接性:每个 30 天窗口一个身份,跨窗口身份互不相关。
- 文档记录的残留(完整分析见 git 历史):信封稳定、低熵的
resource元组加上连续日时段,在少数配置下本就可能桥接窗口,因此这个边界只是抬高成本,而非一道墙。 - 扼杀它的产品需求:跨窗口连续性恰恰是留存分析所需要的。一旦稳定身份成为要求,一个主要给诚实分析添麻烦、只给决心坚定的关联者抬高成本的边界,被判定为错误权衡。
存储中没有 salt,没有轮换计划,管线中没有任何地方有派生标识符。
A.4 重置行为 {#a4-reset-behavior}
删除 $HERMES_HOME/telemetry/shared_metrics 仍如上文所记录那样重置本地身份、聚合和包文件。现在有两点如实的限定:
- 重置重新生成
install_id,因此后续包传输一个新身份。本地重置确实给出一个新的远程身份。 - 重置不能撤回已发送。已传输的包仍以其被发送时所用的标识符留在 ingest 服务的存储中。v1 契约中没有回读或删除 API。
设 send: false 立即停止传输:每个包之前都重新读取同意,因此一次已在飞的传递在它当前正在发的那个包之后停止,而非排空整批。它不删除此前传输的包,也不停止本地采集。
关闭发送还会关闭同意窗口——在实际观察到同意的最后一刻,而非墙上时钟。时段落在一个窗口和下一个窗口之间的包永不传输,即使发送后来被重新开启;这对任意次数的开/关循环、对没有进程运行时的手改、以及对任意方向跳变的时钟都成立(窗口开启被钳制在存储中已有的每个时间戳之上;观察标记每次调用按有界步长推进,因此一次错乱的前向采样不能把确认地平线拖出数年;关闭永不会落在关闭观察自己的时钟之后)。与早先的单一移动加入日期不同,关闭再开启不会丢弃此前已同意窗口中仍未投递的积压——那些包留在它们自己的区间内,保持有资格。
一个刻意的升级路径后果:在区间同意模型之前(send_consent_windows 存在之前)导出的包早于第一个记录窗口,因此升级后永不传输。这是失败即关闭的方向——重新导入旧的移动日戳来放行它们,会把五轮评审证明为不健全的语义重新导入——代价至多是未投递积压,绝不包括已采集数据。
A.5 保留 {#a5-retention}
- 本地: 不变——成功导出的历史保留 30 天,待处理 delta 保留到导出。发送状态不延长本地保留:一个永远发不出去的包仍在 30 天被清理。面对一个永久不可达的端点而本地无界增长,比丢失一台已坏一个月的安装的指标更糟糕。
- 远程: 原始包在生产 S3 中无过期保留,在预发环境保留 30 天。
A.6 删除 {#a6-deletion}
v1 契约中没有远程删除路径,本附录也不发明一个。用户能做的:
| 动作 | 效果 |
|---|---|
send: false | 不再有包离开机器 |
enabled: false | 采集停止;既有本地状态保留 |
删除 .../shared_metrics | 本地身份、聚合和文件重置;后续发送使用新 install_id |
| 删除已发送数据 | 非自助——需要操作员对 S3 bucket 操作 |
若将来承担"应请求删除"的义务,查找路径现在是直接的:用户的 install_id(可从其本地存储读取)就是其数据存储所用的键。构建服务端删除 API 仍是新的产品决策,而非实现细节。
A.7 outbox 目录是什么 {#a7-what-the-outbox-directory-is}
记录在此,是因为它在第 2 阶段规划中被误读一次,那种误读本会删除用户数据。
该目录是本地历史,不是发送队列。package_outbox 是 SQLite 表;其 exported_at 列意为"写到磁盘",而非"已发送"。文件不可变,且仅按年龄清理。
ingest 契约说发送方应在收到 202 时从其 outbox 删除一个包。导出器不这么做。 按确认删除会把用户 30 天的本地历史改作传输队列,摧毁曾向他们承诺的状态。发送状态改放在 package_outbox 表上的新列里;文件不受传输影响。
A.8 范围说明 {#a8-scope-note}
包正文内的 install_id 字段按生成器写入的样子传输(从行上冻结的 sent_install_id 重写,后者记录同一值)。没有其他载荷字段变化,不新增任何内容,服务端把整个正文当作不透明处理。因此载荷 schema 演进如前,仍是发送方一侧的事。