fix(usage): deliver CLI telemetry without next-day return - #5271
Conversation
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
cocolord
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES — [P1] exact head 1e58f13170e35aa1cf5d5fe2ba9cf72d99b8b07f 确实把 CLI aggregate 从“次日有新调用才发送”改成“首个结果立即发送、后续活动最多每 15 分钟一批”,但这个会增加网络时序关联机会的默认行为变化没有更新 notice revision,也没有进入实际首次告知文案。已有 v3 notice 的安装升级后不会再次看到告知,并会在下一条命令完成时立即发送旧缓存和新结果。
动机
这个 PR 要解决的问题真实且边界清楚:旧实现只在 UTC 日切换后的下一次合格调用发送上一日 CLI 计数,短期安装或第二天不再运行的用户可能只上报 heartbeat,永远不交付任何命令结果。把首个完成结果在当日发送、后续按活动触发的 15 分钟间隔发送 delta,能明显提升这类有损诊断的可用性,同时不增加后台 timer、持久网络队列或 payload 身份字段。
本地 exact-head 验证确认了这个正向目标:首个成功/失败结果可当日送达,heartbeat 与 aggregate 独立认领,15 分钟间隔跨 UTC 午夜仍生效,失败不重试,旧 buffer 可读取,disable、环境 opt-out、consent_required 和损坏状态仍 fail closed。不过,这也是一个从每日批次变成活跃期间高频批次的隐私/用户体验变化;PR 文档自己明确承认更频繁请求会增加网络时序关联机会,因此不能只更新设置详情和参考文档而沿用旧告知身份。
改动思路
运行时继续复用 usage_statistics.ts 作为唯一策略、状态、锁与 I/O owner。Python begin/finish 只读取调度提示并把固定 allowlist observation 交给 detached TypeScript CLI;observe 在同一短锁中持久化 heartbeat 与 aggregate claim、消费 batch 并发起请求,网络等待在锁外。新字段 aggregate_last_attempt_ms 保存最近一次 aggregate attempt,替代“日切后 flush”的条件;collector 仍只累加无安装 ID、版本、事件时间或 join key 的 delta,不需要 schema migration。
正向路径是:用户已完成有效告知且未 opt out → CLI begin 返回 generation → 命令结束后 finish 产生一个固定 counter → detached usage_statistics_cli.ts 调用 observe → 首批立即从本机 state 消费并 POST /v1/aggregate;后续结果先缓冲,满 15 分钟后的下一次活动再发送。负向路径也保持集中:blockedBy 统一处理 explicit disable、环境变量、CI、consent_required、endpoint 和 notice;无效 generation、未来 day、时钟回拨、锁竞争或非法 payload 均不发请求。
架构方向是合适的,没有引入第二 sender 或 scheduler。问题在升级边界:sameNotice 只比较 {version, endpoint, policy},而 TypeScript 的 NOTICE_VERSION 和 Python warm-path 检查都仍为 3。于是 cadence/disclosure 文本虽然变了,状态机仍把旧 v3 acknowledgment 当作当前有效授权。
具体改动
完整 PR 修改 9 个文件、+246/-43。生产代码集中在一个 runtime 文件和一个 Dashboard 设置详情组件;文档更新 collector、英文/中文 usage-ping contract;验证新增 133 行 delivery suite、扩展既有 TypeScript/Python 测试,并把新 suite 纳入 control-plane typecheck。没有 generated 文件、collector schema、CLI 参数或新权限面。
关键代码讲解
observe:新增AGGREGATE_INTERVAL_MS和aggregate_last_attempt_ms。counter 入 buffer 后,如果从未尝试或已满 15 分钟,就在 I/O 前把当前 counters 形成 delta、清空本地 batch、记录 attempt 时间,并在锁释放前启动 aggregate request;这能避免竞争 observer 重复 claim,但继续采用有损、失败不重试语义。blockedBy/sameNotice/automaticNoticeRequired:仍是所有发送通道的 policy gate。notice identity 只含 version、endpoint、policy;cadence 不在 identity 中,因此行为变化必须通过 notice version migration 才能让已有安装重新进入 notice-before-send。当前 head 没有做这个迁移。- Python
begin:warm command 直接以notice.version == 3作为已告知提示,然后返回 generation;它不会调用 status 来发现 disclosure 文本已改变。因此只改 TypeScript 文案也不够,Python 常量必须与新 notice revision 同步。 usage_statistics_delivery.test.ts:覆盖首次发送、15 分钟 spacing、跨午夜、旧 buffer、失败丢数、回拨、并发、非法 timestamp 与 malformed aggregate,核心 delivery 语义覆盖较完整;但“已有 v3 acknowledgment 升级到新 cadence”被写成直接 flush 的正向 case,没有独立判断这次隐私变化是否需要 renewed notice。- Dashboard settings 与中英文文档:准确说明首批立即、后续至少 15 分钟一批、unsent tail、接收日偏差和时序关联风险;collector README 也正确说明同日多批 delta。可见的自动首次告知组件和 CLI disclosure 字符串却没有同步 cadence/关联风险。
对主干的风险
[P1] 已接受 v3 告知的升级用户会在没有 renewed disclosure 的情况下进入更高频发送。触发条件很普通:本机已有 loopx_usage_ping_state_v1、notice.version = 3、默认 opt-out acknowledgment 和尚未发送的 daily counters。当前 inspect 返回 automatic_notice_required=false、notice_required=false、sending=true;随后一次 observe 立即向 /v1/aggregate 发送旧缓存 7 条和新结果 1 条,并清空缓存。对同一个 state 和输入,immutable base 不发请求,只把新结果继续留在本机。这证明差异来自本 PR,而不是旧行为。
风险不是 payload 多了身份字段——strict allowlist 与 collector 测试确认没有;风险是发送时间从低频次日批次变为首结果立即、活跃时每 15 分钟,外部网络元数据因而更容易与用户活动相关联。PR 文档已经承认这个 tradeoff,却仍保留 notice v3。更进一步,CLI inspect().disclosure 和 App 的自动 UsageStatisticsNotice 仍只说会发送汇总,不显示新 cadence;设置详情虽然更新,但首次告知在用户进入详情前就会自动 acknowledge。
最小修复是:在 TypeScript 与 Python 中一致 bump notice revision;把首结果立即、15 分钟 cadence 及其网络时序关联 tradeoff 放进真正会被展示并确认的 CLI/App first-use disclosure;保证 renewed notice 被确认前 observe 不发送或消费旧 buffer。增加一个升级回归:seed 已确认的 v3 state 和旧 counters,断言新 head status 要求重新告知且 command/observe 零 POST;完成可见 v4 acknowledgment 后才允许发送。另保留 consent_required 的反例,证明 default 用户仍必须 explicit enable,单纯 acknowledgment 不能授权。
验证方面,exact-head usage TypeScript suites 44/44、collector 12/12、Python 真实 CLI/HTTP tests 21/21、control-plane typecheck 均通过;干净 tracked tree 的 risk-based premerge 通过 4 项 direct、10 项 catalog、8 项 risk-profile 与 public-boundary scan。远端 Frontstage/Release build 与 chat-bundle 的 workspace_ref: "current" 失败在 immutable base 上以同一文件、行号和断言复现,且当前 main 已有独立 #5265 修复;stage2c/checks/pytest/merge-gate 是其 skipped/failure 聚合,属于与本 PR 无关但仍需 rebase 后恢复绿色的 merge-readiness hold。
语义与 CI 对齐
typed state 与 delivery owner 仍在 TypeScript,domain wording 保持通用,at most once every 15 minutes 也被实现为明确机器约束而非模糊 guidance。真正违反的是既有 notice-version contract:仓库已经用 version migration 表达 expanded disclosure,当前却对一个明确改变发送时机和隐私 tradeoff 的默认行为复用 v3。远端 CI 红灯不证明这个 finding,但本地 base/head 升级态反例直接证明它。
我的整体评价
delivery 机制本身是紧凑、可逆且值得保留的:它复用现有 owner,只增加一个经过校验的 attempt timestamp 与聚焦测试,解决短期安装没有有效 CLI 样本的问题;对显式 disable、policy、payload、失败隔离和 collector 边界的证据也较扎实。future-facing pass 不需要新的 abstraction,应该集中在把 cadence 纳入现有 notice migration/visible disclosure owner。
但在修复 renewed disclosure 前不能 approve。long-horizon 上 delivery coverage 改善,user experience/privacy 上却对已有用户发生未重新告知的默认行为回归;这是发布后立即生效且难以由用户察觉的边界。请完成 notice revision、真实 first-use copy 和升级/consent-required 回归,再基于 rebased exact head 重跑 focused suites、premerge 与远端 CI。
English verdict: REQUEST_CHANGES — exact head 1e58f13170e35aa1cf5d5fe2ba9cf72d99b8b07f correctly delivers the first CLI aggregate immediately and later deltas at 15-minute activity intervals, with strong focused coverage and unchanged identifier-free payloads. However, it keeps notice version 3 and the actual CLI/App first-use disclosure omits the new cadence. A seeded acknowledged-v3 upgrade state reports no notice required and immediately POSTs retained plus new counters, while the immutable base sends nothing. Bump the notice revision in TypeScript and Python, disclose cadence/timing-correlation before acknowledgment, block pre-ack sends, and add upgrade plus consent-required regressions. The current dashboard CI failure reproduces identically on the base and is a separate rebase/merge-readiness hold.
…batches Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
cocolord
left a comment
There was a problem hiding this comment.
APPROVE — exact head f9735814e09020113eb73eb13354dfaaa432e84e 已完整修复上一轮 renewed-disclosure blocker。通知版本在 TypeScript 与 Python 同步升到 v4,CLI 和 App 的真实可见告知都明确写出首个结果立即发送、后续活动至少间隔 15 分钟,以及网络时序关联风险;v3 升级在确认前不发送、不消费旧缓存,确认后清理旧 scope 并轮换 generation。基于 whole-PR diff、正负路径和 base/head 对照,本轮没有发现需要作者继续修改的 blocker。
动机
旧实现要等到 UTC 日切后的下一次合格调用才发送上一日 CLI aggregate,因此短期安装或次日不再运行的用户可能永远没有命令结果样本。这个 PR 将首个 measured result 改为立即尝试发送,后续 delta 在有新活动时至少间隔 15 分钟发送,能明显改善统计可用性,同时继续保持无后台 timer、无持久网络队列、无安装 ID 的有损 aggregate。
这个收益伴随真实的隐私边界变化:活跃安装的请求频率提高,网络服务更容易通过 IP 和请求时间关联活动。当前 head 没有回避这个成本,而是把它纳入既有 notice migration,并在实际首次告知、设置详情和中英文文档中一致披露。
改动思路
实现继续让 usage_statistics.ts 作为策略、状态、锁和 I/O 的单一 owner。Python begin/finish 只负责命令入口适配和 detached 调用;Dashboard 只消费同一 settings API;collector schema 与 identifier-free payload 均未变化。新增的 aggregate_last_attempt_ms 是一次 attempt watermark:首批立即 claim,后续只有在间隔满足且有活动时 claim;batch 在网络 I/O 前持久化消费,失败不重试,保持明确的 lossy 语义。
修复后的升级路径是:旧 v3 acknowledgment 被识别为 stale notice → 所有 channel 在 v4 acknowledgment 前保持 blocked,旧 buffer 不被消费 → 可见 CLI/App notice 告知新 cadence 和 timing tradeoff → acknowledgment 清除旧 scope counters 并轮换 generation → 只有新 generation 的后续观察可以发送。consent_required 下 acknowledgment 仍不等于 explicit enable,explicit disable 和环境 suppressor 仍优先。
具体改动
完整 diff 为 13 个文件、+378/-54。生产改动集中在 TypeScript usage owner、Python CLI hint 和两个 Dashboard 组件;其余是 collector/中英文文档、focused TypeScript/Python tests 与 typecheck 配置。没有新增 sender、scheduler、身份字段、collector schema、权限或 CLI 参数。
关键代码讲解
NOTICE_VERSION/_NOTICE_VERSION:TypeScript 与 Python 同步为 4,使 cadence/privacy disclosure 成为真实的 typed migration boundary,而不是仅更新文案。blockedBy、sameNotice与configure:继续统一拥有 consent、notice、endpoint、环境策略和 generation fencing;旧 v3 state 在 v4 确认前无法触发 heartbeat 或 aggregate,确认时丢弃旧-scope counters,避免跨 disclosure scope 发送。observe:在短锁内记录 counter、认领 heartbeat/aggregate、更新 attempt watermark 并消费 batch;网络等待仍在锁外。并发 observer、disable、失败、时钟回拨和 malformed state 都有明确分支。usage_ping.begin与UsageStatisticsNotice:CLI warm-path 使用相同 v4 版本,App 自动告知新增 cadence 与网络时序文本;这两个才是首次确认路径,设置详情和参考文档只是补充说明。
验证覆盖了行为而不只覆盖新 helper:focused TypeScript usage suites 36/36、Python 真实 CLI/source entrypoint 63/63、严格 TypeScript typecheck、Dashboard acceptance 均通过;risk-based premerge 通过 5 个 direct checks、10 个 catalog checks、8 个 risk-profile checks 和 public-boundary scan,无 warning 或 manual hold。升级负路径验证了 v3 pre-ack 零 POST、旧 buffer 不消费、错误 notice 拒绝、ack 后 generation fencing,以及 consent_required 不能由 acknowledgment 越权启用。
对主干的风险
剩余产品风险是设计上明确接受的:aggregate 仍是有损的,失败 batch 不重试,安静会话可能留下 unsent tail;活跃安装可能每 15 分钟产生一次请求,尽管 payload 无 ID,网络元数据仍可能体现活动时序。当前实现通过 v4 renewed notice、设置/文档、用户随时关闭和严格 payload allowlist 把这个风险公开并限定在既有 machine-wide telemetry authority 内。
GitHub 当前仍有三个与本 PR 改动面无关的红灯,因此 merge readiness 应继续 hold,不能因本 review 的 APPROVE 而绕过:两个 Python quota-settlement assertion 在 PR exact head 和 immutable merge-base 7cc95d110d3c92f074b819036c76d08adcc5f6a8 上用相同 pytest 用例复现了相同失败,且 quota 代码/测试不在 diff;typescript-core (3/3) 的两个 host_process.test.ts descendant-cleanup timing assertion 同样不在 diff,本地 exact head 与 base 各连续 6 次均 9/9 通过,表现为 runner timing-sensitive failure。后者没有 immutable base 的同环境红灯证据,所以这里只把它标为独立 CI 重跑/转绿要求,不宣称已经严格证明为既有失败。
语义与 CI 对齐
typed state 和 effect authority 仍由 TypeScript 单点拥有;没有 substring denylist、领域特化的通用控制面文案或把 machine-enforced obligation 写成 guidance。行为变化已在真实 notice、设置和双语文档中披露,旧 notice 不能授权新 cadence。APPROVE 表示 exact-head diff 未发现作者需要修复的阻塞项,不表示授权 merge,也不替代 required checks。
我的整体评价
这是一个收益明确且范围合适的改动:它直接减少“短期安装没有 CLI 样本”的系统性缺口,复用现有 owner,只增加一个经过约束的持久 watermark,并对失败、并发、升级、consent 与可见告知给出了足够的正负证据。上一轮 blocker 已在真实入口和迁移状态上修复,而不是只改测试期望。future-facing pass 也已到位:没有必要再增加并行 sender 或抽象;后续若调整 cadence,仍应沿用同一 notice-version owner。
因此我对 exact head 给出 APPROVE。合并前仍应让 GitHub required checks 通过或由其责任方按仓库规则处置上述独立失败;本 review 不授予 bypass 或 merge authority。
English verdict: APPROVE exact head f9735814e09020113eb73eb13354dfaaa432e84e. The renewed v4 disclosure now precedes the faster cadence across the real CLI and App paths, stale buffers and generations are fenced, consent-required policy remains explicit, and focused positive and negative coverage validates the changed invariant. The current unrelated red checks remain a separate merge-readiness hold and must not be bypassed on the strength of this approval.
CLI telemetry previously depended on a later UTC-day invocation, so a short-lived installation could report its heartbeat but never deliver any command results. This change attempts the first measured result immediately and sends subsequent buffered deltas on eligible activity at least 15 minutes apart, including across midnight.
The existing TypeScript usage-statistics owner retains consent, opt-outs, local locking and the identifier-free aggregate payload. Heartbeat and aggregate claims start their requests under the same short lock after persistence; network waits stay outside it. Existing buffer shapes remain readable. Notice revision 4 renews disclosure in both the TypeScript owner and Python entrypoint before the faster cadence applies. CLI stderr and the visible App notice explain immediate first delivery, the 15-minute activity interval and network timing correlation. Before acknowledgment, old buffers are untouched and no channel sends; acknowledgment discards old-scope counters and rotates generation, fencing queued old observations. Explicit disable and consent-required policy retain precedence. No collector migration or new configuration is required. App Device defaults and the English/Chinese documentation explain the new cadence; Goal-duration snapshots retain their daily schedule.
Validation:
loopx canary premerge --from-git-diffpassed: direct checks, 10 catalog checks, eight risk-profile checks and the public-boundary scan; no failures or manual holds.Tradeoff: this is still lossy, activity-triggered diagnostics. Quiet sessions can leave an unsent tail, failed batches are not retried, and aggregate receipt dates/versions cannot be joined to installation counts. Active installations may send up to one batch per 15 minutes instead of one per day; more frequent requests can expose more network timing correlation. Payload fields and recipient authority are unchanged.
Future-facing pass: reused the existing typed owner and locking seam, removed the aggregate dependency on the daily heartbeat claim, and added no parallel sender, scheduler, version dimension or identity field.
The branch incorporates main through
7cc95d110, including the separate dashboard build fix referenced in review. The renewed-notice correction is commitf9735814e.This changes runtime behavior and is left for maintainer review and merge. It does not update installed clients or deploy a release.