HEL-896 release completion: the A/B/C budget becomes an executable two-instance split, the real-browser false positive is fixed precisely (product untouched), the SDK gap list is corrected to the honest 41-distribution closure with the pip-download erratum, and release archiving becomes a consistency snapshot - (1) budget split: the manager's independent review reproduced on a temp store that writing A's total_requests=55 into the shared product ledger blocks B's mem0_extract and C's dialogue_main with budget_total_requests_exhausted:55/55 while budget_total/request_usage/token_usage have no stage or authorization-source column; docs/budget-package-hel895.md gains §6 making the split executable: A lives ONLY on the isolated 8360 team-test instance with its own DB and models.yaml (55/500000 one-shot totals plus same-value daily category caps), the user instance NEVER carries one-shot totals (its budget_total rows are record-only, no gate), A is sealed by stop+consistent-snapshot archiving with budget_total never zeroed and restart/archive proving the seal holds, old test data has no import path into the user instance (release checklist forbids copying the 8360 data dir), B starts only after baiqizhi approval writes llm_cap/embed_cap per the 500+180 formula with the authority frozen prefix, the real-Mem0 smoke is counted INSIDE B's 60/180, B cap-hit queues batches (no retry burn, no lost rows, FTS fallback, chat uninterrupted) and first-week end requires a fresh approval to continue, and §6.4 gives one responsible limited recommendation (A 55/500k isolated, B 60 LLM/180 embeds, C daily table) plus errata for the two unsafe original claims ('A shares C's daily account' and 'WAL single file copies whole'); tools/verify_budget_split_hel896.py 8/8 on temp stores and stand-ins only (S0 exact-shape repro of the manager's finding, S1 team instance consumes the §1 step plan to 55 then hard-stops with the tightest gate winning - life daily 8/8 rejects before the total, S2 seal survives restart and backup-API archive, S3 user instance B/C unblocked after A with dialogue 60/semantic 40/daily-token stacking and rejections booking nothing, S4 the misconfigured-total guard on the user instance, S5 repo configs and authoritative facts untouched, real caps all stay 0); (2) browser false positive fixed without touching any product file: web/tests/hel895_real.mjs B3 used '!!stage || any visible button' and clicked the FIRST button containing 回 which is the 日子与回忆 home nav button, so stage=false still PASSed and the screenshot stayed on days - the verbatim original rerun reproduces the false 7/7 with B3 stage=false (docs/rework/hel896/browser-before/), and the revised test passes 9 checks by precise interaction: B2 asserts the letter card carries her exact words plus the talk-nav unread aria-label, B3a waits for the VISIBLE reach card (aside.reach.show, aria-hidden=false, non-zero box, exact words) over days, B3b clicks only THAT card's 回她 button and strictly asserts the conversation stage .conversation .sr contains the reach-out text with the unread mark cleared, B4/B5 prove dedup by server message id (data-mid in the record panel: zero duplicates, gap and live reach-outs each exactly once), B5 asserts /api/health epoch bumps exactly +1 (1->2) before rechecking, B6 counts the page's OWN permissions.query/getUserMedia calls from first document load (0/0, instrumented via Page.addScriptToEvaluateOnNewDocument after Page.enable), B7 runs the service under a temp loopback models config (CYBERGRIL_MODELS, all caps 0) plus a sitecustomize socket guard that REJECTS and eagerly reports non-loopback connects (0 attempts, 0 non-loopback DNS lookups, report survives signal death) - under precise interaction the stage really plays, unread really clears, dedup and epoch semantics really hold: it was a test defect, not a product defect, so no interface change and nothing to escalate; (3) SDK honesty: tools/audit_sdk_offline_hel896.py (read-only, zero downloads/installs/inference) computes the 3.11-Linux closure from the installed 3.10 pinned metadata - 55 dists, 41 missing from the product venv (28 pure + 13 native incl. grpcio/numpy/onnxruntime/tokenizers/sqlalchemy/protobuf), all three top-level pins are py3-none-any (no 'cp311 wheel' needed for them), mem0ai's installed direct_url is the local fixed source hel857-src and hel857-data/mem0-2c81af3.tar.gz hashes exactly to the lock's 9dcef930...dda23 (an index mem0ai==2.2.1 wheel proves nothing), the wheel built during hel857 lived only in an ephemeral cache and hatchling is not offline-available, exactly 3 of the 41 are cache-reusable today for 3.11 (greenlet 3.5.6 cp311, pillow 12.3.0 cp311, truststore 0.10.5 py3-none-any - version+tag verified), the fastembed model cache is measured (Qdrant/bge-small-zh-v1.5 + bm25, ~92MB, placement = instance data dir copy), and the seq48-51 pip event is settled by cache forensics: uv's pip-26.2.2 wheel entry (files.pythonhosted.org, sha256 fd7f4dfa...) was CREATED 2026-10-27 02:10:04 inside the HEL-895 run window as the ONLY new cache entry, so that step DID fetch from the network - the prior card's zero-download claim is corrected to 'download' by the conservative reading (byte-level download vs conditional request is indistinguishable from cache evidence) while the lock install itself added no new cache entries; deploy/hel895-release-candidate.md §4 is rewritten to this accurate list, all future installs forced offline/no-index; (4) release archiving: tools/release_snapshot_hel896.py implements the two safe modes (stop+wal_checkpoint(TRUNCATE)+copy, or online SQLite backup API), writes per-file sha256 manifests carrying the exact code commit, per-table row-count verification and a verify-mode rollback drill; its selftest reproduces the manager's WAL finding (online naive copy of the main .db: 'no such table: companion') and proves both fixed modes complete (backup API and checkpoint copies both hold companion=1/chat_message=1, archive hashes+rows verify, commit 04eff61 recorded); §5 of the release doc now carries the copy-paste commands covering the product DB, semantic vector root, history root and the authoritative embed ledger, archives are never deleted, and the section explicitly does NOT claim R6 complete backup; regression on this head: pytest 299/299 (3.11.16 venv + requirements.lock offline), ruff clean (src/tests/tools), web typecheck 0 errors / lint 0 errors 1 pre-existing warning / vitest 55/55 / build passes with CSS still index-DdDYzR5S.css, pinned-SDK reruns all green (close-ownership 16/16, shutdown-queued 12/12, inflight 6/6, semantic-recovery 7/7, mem0-real 12/12, reports in docs/rework/hel896/regression/), check_release_offline R1-R7 passes, real-HTTP pagination 7/7 rerun saved as docs/rework/hel896/http-pagination-rerun.json with the original hel895 evidence byte-untouched, requirement checks 6/6 (origin/main, de8874a3803b, 152479b, each with and without handoff-HEL-896.json); handoff-HEL-896.json written; zero real LLM, zero real embeddings, zero new downloads beyond existing caches, authoritative ledgers and freezes (39/60, 498/500, 2 frozen, HEL-835 gate) untouched, not deployed, PR#11 appended-not-merged, no visual changes, R5/R6/R7 honestly deferred

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
zonggong
2026-10-27 03:06:23 +08:00
co-authored by multica-agent
parent 04eff611e7
commit c121c42e39
31 changed files with 3031 additions and 56 deletions
+67 -16
View File
@@ -1,5 +1,13 @@
# HEL-895 · 统一发布候选(离线核验过,未执行部署)
> **HEL-896 修订(2026-10-27,正常追加 PR#11)**:按总管独立复审补齐——①浏览器
> 「真实服务 7/7」中 B3 原为已证实误报,测试已修为精确交互+严格断言(9 项检查,
> 含权限 API 调用计数与非回环出站守卫,证据 `docs/rework/hel896/browser/`);
> ②A/B/C 预算改为独立团队测试实例 + 全新用户实例的可执行分账
> (`docs/budget-package-hel895.md` §6);③固定 SDK 缺项勘误为 41 项闭包清单
> (§4);④归档/回退改为一致性快照口径(§5)。候选 head 与交付物不变,未部署、
> 未合并、未写权威账。
**状态:候选。审核通过与预算/部署授权前不启动面向用户的真实循环、不部署、不合并
PR、不交体验地址。** 本文件把已通过独立复审的内核(HEL-894 @ `152479b`)与
HEL-881 已审核的主动留言前端(`bf234570`)装成一个统一候选,给出精确 head、
@@ -24,7 +32,10 @@ HEL-881 已审核的主动留言前端(`bf234570`)装成一个统一候选
(`tools/verify_http_pagination_hel895.py`,真实服务 + 327 行播种 + fetchAfter
缺口游标移植对真实后端复验);**真实服务浏览器 7/7**(`web/tests/hel895_real.mjs`,
Chromium CDP 对真实 API:留言卡、舞台播放、260 行缺口不跳留言、重启 epoch 去重、
零权限申请)。证据 `docs/rework/hel895/`。双目标及实际母线需求检查各含/不含
零权限申请。**HEL-896 勘误:原 7/7 中 B3 为已证实误报**——兜底断言+点击
「日子与回忆」导航钮,`stage=false` 仍 PASS;修订版改精确交互+严格断言共 9 项
检查全过,含权限 API 调用计数 0 与非回环出站守卫 0,证据
`docs/rework/hel896/browser/`,原误报证据保留于 `docs/rework/hel896/browser-before/`)。双目标及实际母线需求检查各含/不含
handoff-HEL-895 共 6/6 通过(origin/main、原产品目标 de8874a3803b、实际母线)。
## 1. 交付物清单
@@ -34,10 +45,11 @@ HEL-881 已审核的主动留言前端(`bf234570`)装成一个统一候选
| 代码 | 分支 `hel-870-assembly`,精确 head 见交付评论 | 含本文件、`docs/budget-package-hel895.md`、`tools/verify_http_pagination_hel895.py`、`web/tests/hel895_real.mjs` |
| Python 依赖 | `requirements.lock`(19 项固定版本,uv 离线可装) | Python 3.11(发布机 3.11.16 实测) |
| 前端产物 | `web/`(原作者界面 + HEL-875/880 主动留言) | `npm ci && npm run build`;CSS 哈希与已审核版一致 |
| mem0 固定栈 | `deploy/mem0-stack.lock`(mem0ai 2.2.1 / qdrant-client 1.20.0 / fastembed 0.9.1) | **3.11 wheel 仍未落位,见 §4;缺项不阻塞其余发布** |
| mem0 固定栈 | `deploy/mem0-stack.lock`(mem0ai 2.2.1 / qdrant-client 1.20.0 / fastembed 0.9.1) | **3.11 缺 41 项闭包,见 §4;缺项不阻塞其余发布** |
| systemd 模板 | `deploy/cybergril-hel874-8360.service.example`(PYTHONPATH=src 已内置) | 独立单元/端口/数据目录模式 |
| 配置模板 | `config/models.yaml`(预算全 0)+ `config/semantic.example.yaml` | 批准值写入位置见 `docs/budget-package-hel895.md` |
| 预算执行包 | `docs/budget-package-hel895.md` | A/B/C 一次批准核定稿,默认 0 |
| 配置模板 | `config/models.yaml`(预算全 0)+ `config/semantic.example.yaml` | 批准值写入位置见 `docs/budget-package-hel895.md`(A 仅团队实例,§6) |
| 预算执行包 | `docs/budget-package-hel895.md`(含 §6 HEL-896 分账修订) | A/B/C 一次批准核定稿,默认 0 |
| HEL-896 工具 | `tools/verify_budget_split_hel896.py`、`tools/audit_sdk_offline_hel896.py`、`tools/release_snapshot_hel896.py`、修订版 `web/tests/hel895_real.mjs` | 分账切换验证、SDK 余缺只读审计、一致性快照/回退验证、严格浏览器检查 |
## 2. 安装命令(发布卡执行)
@@ -58,18 +70,36 @@ weather/materials/backup=false。**主动留言前端已并入本候选:生活
## 4. 固定 SDK(3.11)准确缺项与不下载路径
**本卡离线核实(2026-10-27)**:产品 3.11.16 venv 仅含 `requirements.lock`,
`import mem0/qdrant_client/fastembed` 均缺;本机 uv(974M)与 pip 缓存中**没有**
任何 mem0ai/qdrant-client/fastembed 的 wheel(任意 Python 版本);固定安装
hel857-venv 是 Python 3.10.12 + mem0ai 2.2.1/qdrant-client 1.20.0/fastembed
0.9.1(本卡全部 SDK 回归仍在其上实跑,3.10/3.11 site-packages 未混用)。
**缺项清单(发布卡落 wheel 时照抄)**:`mem0ai==2.2.1`、`qdrant-client==1.20.0`、
`fastembed==0.9.1` 的 cp311/manylinux2014_x86_64 wheel(及其纯 Python 传递依赖
wheel,pip download 会一并解析);目标平台 x86_64 Linux(部署机 `uname -m`
复核);锁定来源 `deploy/mem0-stack.lock`(源码身份 2c81af3 / tarball SHA256 见
lock 注释)。下载与离线并入命令、sha256 留证、co-import 最终判据不变,照
`deploy/hel874-release.md` §4 执行。**在 wheel 落位且 co-import 通过前,不得以
stub/FTS 冒充真实 Mem0 体验就绪;本卡未下载任何内容。**
**HEL-896 只读审计修订(2026-10-27,`tools/audit_sdk_offline_hel896.py`,证据
`docs/rework/hel896/sdk-audit/`)**:此前「只缺 mem0ai/qdrant-client/fastembed 三个
cp311 wheel 及纯 Python 传递依赖」的概括**不准确**,勘误如下:
- 三个顶层包固定版本(mem0ai 2.2.1 / qdrant-client 1.20.0 / fastembed 0.9.1)的
WHEEL 均为 `py3-none-any` 纯 Python,本身无 cp311 专用轮一说;**mem0ai 的安装来源
是本地固定源码**(hel857-src,`deploy/mem0-stack.lock` 源码提交 2c81af3、tarball
SHA256 `9dcef930…dda23` 已在本机 hel857-data 实测一致)——从索引下载同名 2.2.1
wheel 不证明与固定源码相同,发布必须用固定 tarball/源码离线构建安装。
- 按 3.11 Linux markers + extras 从 3.10 固定栈安装元数据求闭包共 **55 项,产品
3.11 venv 缺 41 项**(28 纯 Python + 13 原生扩展:grpcio/numpy/onnxruntime/
tokenizers/sqlalchemy/protobuf/greenlet/pillow 等)。这是**依赖闭包清单**,不是
已完成的 cp311 解析或下载结果。
- 当前本机缓存对 41 项缺项**立即可复用**的只有 3 项(greenlet 3.5.6 cp311、
pillow 12.3.0 cp311、truststore 0.10.5 py3-none-any,uv wheels-v6/v7 缓存实测);
其余 37 项 + mem0ai 固定源码离线构建所需 build backend(hatchling 未在缓存)由
发布卡获授权后按 `deploy/hel874-release.md` §4 拉取,逐项 sha256 留证,下载清单
保持 `deploy/mem0-stack.lock` 与固定源码身份;缺项不阻塞其余发布工作。
- **fastembed 模型缓存实测**:hel857-data/fastembed-cache 含真实模型
`Qdrant/bge-small-zh-v1.5` 与 `Qdrant/bm25`(约 92MB);发布机落位=实例数据目录
内该缓存的拷贝,真实 mem0 模式的 cache_dir 指向它;本卡零下载、不推理。
- **seq48–51 pip 事件勘误(缓存取证)**:HEL-895 运行中去掉 `--offline` 安装的
pip 26.2.2 在 uv 缓存的 wheel 条目创建于 2026-10-27 02:10:04(该运行窗口内,
来源 files.pythonhosted.org,且为窗口内唯一新增缓存条目)——该步发生了网络
获取,前卡「零下载」对此步不成立;requirements.lock 装机同期未产生新缓存条目
(全部命中既有缓存)。字节级完整下载与条件请求无法由缓存取证区分,按保守口径
记为下载。**后续一切安装强制 `--offline`/`--no-index`。**
产品 venv 目前 `import mem0/qdrant_client/fastembed` 均缺、产品代码导入不受影响;
在 wheel 落位且 co-import 通过前,不得以 stub/FTS 冒充真实 Mem0 体验就绪。
## 5. 停用与保留数据回退
@@ -78,6 +108,27 @@ stub/FTS 冒充真实 Mem0 体验就绪;本卡未下载任何内容。**
`systemctl stop` → 数据目录原样保留 → 切回上一 release/上一提交(R4 迁移全部
增量新表,旧代码可安全打开同一数据库);彻底撤销留数据档不删。
**HEL-896 归档/回退修订(2026-10-27)**:此前「SQLite WAL 单文件可整体复制/归档」
的口径**不安全**——在线库只复制主 `.db` 会丢 WAL 里已提交的资料(总管复现件连
`companion` 表都不在;本卡 `tools/release_snapshot_hel896.py` 自测同样复现
`no such table: companion`,证据 `docs/rework/hel896/release-snapshot/selftest.json`)。
发布归档一律二选一:**停服务 + `PRAGMA wal_checkpoint(TRUNCATE)` 后整目录归档**
(推荐),或**在线用 SQLite backup API 逐库一致性快照**。照抄命令:
```bash
# 覆盖范围:产品库(CYBERGRIL_DB)、语义向量目录 vector_root、推导史 history_root、
# 权威嵌入账 ledger_path(按部署实例 config/semantic.yaml 实际路径逐项代入)
.venv311/bin/python tools/release_snapshot_hel896.py snapshot \
--name <release-或阶段名> --db <产品库> [--db <权威账>] \
--file <vector_root> --file <history_root> --out /opt/<实例>/archives/
.venv311/bin/python tools/release_snapshot_hel896.py verify --archive <产物>.tar.gz
```
产物含逐文件 SHA256 清单、**代码精确提交**(快照时刻 `git rev-parse HEAD`)、
逐表行数核对与回退演练结果;不删档,旧归档永久保留。回退=停服务 → 解包快照到
新数据目录 → 指回上一 release/上一提交 → 起 service → `verify` 通过后交付。
本节是**发布归档/回退要求**,不冒充 R6(CG-009 每日自动备份/手动完整导出)完成。
## 6. 本候选不做的事
不合并任何 PR(PR #11 保持开放待总管/白栖知决定);不部署、不交体验地址;
+96 -6
View File
@@ -1,5 +1,13 @@
# HEL-895 · 真实全链一次批准执行包(A/B/C 合并核定稿;仍全部待授权,默认 0 不变)
> **HEL-896 分账修订(2026-10-27)**:总管独立复审证实,若按本文件 §1 原表把 A 的
> `total_requests=55` 写进与 B/C 共用的产品库,B 的 `mem0_extract` 与 C 的
> `dialogue_main` 在 A 用满 55 次后都被 `budget_total_requests_exhausted:55/55` 挡死
> (复现 `tools/verify_budget_split_hel896.py` S0;账表也无阶段/授权来源字段)。
> **执行口径改为 §6:A 落在独立团队测试实例自己的库,B/C 落在全新用户实例的库,
> 用户实例永不配置一次性总量。** §1–§5 与本修订冲突处以 §6 为准;四道闸叠加、
> 保守入账、碰顶停机等语义不变。
本文件把 `docs/budget-plan-hel874.md` 的 A/B/C 三表收拢成**一次可批准的具体执行包**:
每一步的请求/嵌入/失败-取消-重试上限、累计与日顶的叠加口径、一次性团队检查与用户
日常使用的分账、碰顶停机语义与留证方式。技术核对基于本卡实际代码与配置
@@ -14,13 +22,16 @@
## 1. 批准项(一个包,三个独立账)
| 部分 | 用途 | 硬顶(批准后由总管写入) | 账本 |
| 部分 | 用途 | 硬顶(批准后由总管写入) | 账本(HEL-896 修订:见 §6 实例分账) |
| --- | --- | --- | --- |
| A · 一次性团队整体检查 | 8360 隔离实例按体验链 1–7 全链走一遍(stub 语义,零嵌入) | 总量 55 次请求 / 500,000 tokens / 0 嵌入(`budget.total_requests`/`total_tokens`,跨日累计) | `budget_total`/`request_usage`/`token_usage` |
| A · 一次性团队整体检查 | 8360 隔离实例按体验链 1–7 全链走一遍(stub 语义,零嵌入) | 总量 55 次请求 / 500,000 tokens / 0 嵌入(`budget.total_requests`/`total_tokens`,跨日累计)——**只写团队实例自己的库** | 团队实例库的 `budget_total`/`request_usage`/`token_usage` |
| B · 首周语义激活 | `CYBERGRIL_SEMANTIC=mem0` 真实激活观察 | 提取 LLM ≤60 次(`llm_cap`);嵌入 ≤180 条新批 → 按算式写 `embed_cap=680`、`embed_reserve=2`(权威账已用 498/原批 500) | `llm_ops`(新表)/`embed_ops`(权威账续账,`authority` 冻结前缀必填) |
| C · 用户日常使用 | 体验地址交付后每日 | dialogue ≤60/日、life ≤8/日、background ≤40/日、semantic ≤40/日(`daily_requests`);嵌入 ≤60 条/日(`embed_daily`);tokens ≤2,000,000/日(`daily_tokens`,全类别共池) | 同 A 的持久表 + 日顶列 |
| C · 用户日常使用 | 体验地址交付后每日 | dialogue ≤60/日、life ≤8/日、background ≤40/日、semantic ≤40/日(`daily_requests`);嵌入 ≤60 条/日(`embed_daily`);tokens ≤2,000,000/日(`daily_tokens`,全类别共池) | 用户实例库的持久表 + 日顶列(**不配置一次性总量**) |
A 当天同时受 C 类日顶约束(叠加见 §2);B 期间的对话/生活/判定请求并入 C 日账,
【HEL-896 勘误】原文「A 当天同时受 C 类日顶约束」按共用产品账本表述,会把 A 的消费
算进用户的日账并让 A 的 55 总量挡住 B/C;修订后 A 在自己的实例与账本内受**同数值**的
类别日顶约束(§6 S1 验证:life 日顶 8/8 先于总量拒绝、semantic 余量内由总量拒绝——
叠加取最紧),与用户实例日账互不相干。B 期间的对话/生活/判定请求并入 C 日账,
不占 B 的 60 次。三部分独立授权来源保持:A 需总管新批(产品环境网关用量不属于
HEL-857 的 60 次实验授权);B/C 需白栖知明确批准。
@@ -61,8 +72,10 @@ HEL-857 的 60 次实验授权);B/C 需白栖知明确批准。
- 语义层到顶只停语义(检索回落词面 FTS,主对话不中断);日 token 到顶停当日全部
真实调用;A 总量到顶即团队检查收尾。
- 留证:`budget_total`/`request_usage`/`token_usage`/`embed_ops`/`llm_ops` 表 +
服务日志 + 隔离实例数据目录(SQLite WAL 单文件可整体归档);碰顶当时的拒绝
reason 与计数快照截图/导出一并归档到交付评论。
服务日志 + 隔离实例数据目录(【HEL-896 勘误】在线库直接复制主 `.db` 会丢 WAL 中
已提交行——归档一律停服务+checkpoint 或 SQLite backup API 一致性快照,见
`tools/release_snapshot_hel896.py` 与 `deploy/hel895-release-candidate.md` §5);
碰顶当时的拒绝 reason 与计数快照截图/导出一并归档到交付评论。
## 5. 本包默认状态(本卡随包可执行)
@@ -71,3 +84,80 @@ HEL-857 的 60 次实验授权);B/C 需白栖知明确批准。
全部 cap/daily 为 0、`authority` 必填结构就位。**未写入批准值前,任何创建/对话/
生活/gist/判定/成长/主动措辞/提取调用都在发送前被硬闸拒绝(`budget_*_exhausted`),
这是设计行为;已由 `tools/check_release_offline_hel882.py` R3 与 299 pytest 复现。**
## 6. HEL-896 可执行分账与阶段切换(独立团队测试实例 + 全新用户实例)
本节把 A/B/C 落成可直接执行的实例/账本拆分;全部语义由
`tools/verify_budget_split_hel896.py` 在临时库+替身上验证(零 provider、零真实 cap
变更,证据 `docs/rework/hel896/budget-split/budget-split.json`)。不新造预算引擎,
不改表结构:`budget_total/request_usage/token_usage` 无阶段字段这一事实,
正是用实例隔离代替字段拆分的理由。
### 6.1 实例与账本拆分
| | 团队测试实例(A) | 用户实例(B/C) |
| --- | --- | --- |
| 形态 | 8360 端口隔离 systemd 单元(`deploy/cybergril-hel874-8360.service.example` 模式),独立数据目录 | 发布候选部署的全新实例,**全新空库**(用户自己创建唯一真实档案) |
| `models.yaml` | `total_requests: 55`、`total_tokens: 500000` + 当日类别日顶(同 C 表数值)+ `daily_tokens` | **永不配置 `total_requests`/`total_tokens`**(保持注释/缺省);只写 C 表日顶;`budget_total` 行仅作流量记录,无总量闸(S3 验证) |
| 语义层 | `CYBERGRIL_SEMANTIC=stub`(零嵌入、零真实 Mem0) | B 批准后 `=mem0`;`embed_cap/llm_cap/embed_daily/llm_daily` 只在白栖知批准后由总管写入 `config/semantic.yaml`(含 `authority` 冻结前缀) |
| 账本 | 自己的 SQLite(`budget_total` 等);封账后归档留存 | 自己的 SQLite 日账 + 权威 `embed_ops` 续账(498/500 + 冻结 2 条原样) |
| 授权来源 | 总管新批(产品环境网关用量,不属于 HEL-857 60 次实验授权) | B/C 均需白栖知明确批准(一次批准包 = 本文件) |
误配守门:用户实例若误写 `total_requests`,A 型堵死会立刻复现(S4)——发布卡
部署检查单必须逐项核对用户实例 `models.yaml` 无一次性总量两行、团队实例配置只存在
于 8360 数据目录。
### 6.2 A 的执行与封账
1. 总管批准后写入团队实例 `models.yaml`(仅该实例),按
`docs/budget-plan-hel874.md` §1 体验链 1–7 逐步执行:创建/诠释/习惯 4、对话 16、
生活 8、判定/成长/gist 20、主动措辞 3,其余额度备用至 55;每步在四道闸下原子
预占(类别日顶→日 token 池→一次性总量;S1 验证叠加取最紧)。
2. **A 结束即封账**,两种到达方式同义:额度用满(`budget_total_requests_exhausted:55/55`
如实拒绝、不补发)或体验链走完。封账动作:`systemctl stop` 8360 实例 →
`tools/release_snapshot_hel896.py` 做一致性快照(停服+checkpoint/backup API)→
归档(数据目录 + 哈希清单)交交付评论。封账跨重启、跨归档不失效(S2 验证),
`budget_total` 永不手工清零。
3. **旧测试数据不会转成用户伴侣**:测试档案、聊天、生活数据只存在于团队实例库与
归档;用户实例是全新空库,由用户自己创建档案;产品无任何「测试库导入用户库」
路径,发布卡明令禁止把 8360 数据目录复制/合并进用户实例数据目录。
4. 后续任何团队复检 = 新批准 + 新实例/新库(或在 8360 实例上以新批准抬
`total_requests`),绝不续在用户账上。
### 6.3 B/C 的开始与日常
1. **B 开始的配置与授权**:白栖知批准后,总管把 `llm_cap: 60`、按算式
`embed_cap: 680`、`embed_reserve: 2`、`llm_daily/embed_daily`(C 表值)与
`authority`(权威账前缀逐行摘要 + `min_used: 498`)写入用户实例
`config/semantic.yaml`,`CYBERGRIL_SEMANTIC=mem0` 生效;缺/错 authority 在构造前
fail-closed(HEL-885 既有验证,不在本卡重复)。
2. **真实 Mem0 冒烟与 B 首周的关系**:A 的 stub 检查证明产品链路零语义也能走通;
B 激活当天先做**真实 Mem0 冒烟**(数条 add/search/原话回读),其 LLM/嵌入消费
全部计入 B 的 `llm_ops`/`embed_ops`(即 60/180 的一部分,不另设隐藏额度),冒烟
通过即进入首周观察;B 期间用户对话/生活/判定照走 C 日账,与 B 的累计账并行不占额。
3. **B 到顶的停/续语义**:任一 cap 到顶 → 作业批回 `queued`
(reason=`semantic_*_budget_*`)不丢行不耗重试,语义层停(检索回落词面 FTS,
主对话不中断);**续跑唯一路径 = 新批准按算式抬 cap**(`embed_cap = 原上限 +
新增量`),总管写入后同批续跑,已完成行不重发。无新批准则语义层停在原地,
不自动翻页、不挪用 C 额度、不动冻结余额。
4. **首周结束的停/续语义**:7 天观察期满,总管导出 `llm_ops`/`embed_ops`/日账实际
用量与质量观察报告交白栖知;未获新批准,B 保持当时 cap(用尽即停语义),
C 日常照常;获批续期则按新算式写新 cap。B 与 C 无任何自动转换或继承。
5. **全局累计与日顶的叠加**(仅用户实例内部):每笔真实发送四道闸原子预占、取最紧
(类别日顶 → 日 token 池 → B 的累计 cap×2 → embed/llm 日顶);A 的总量闸不在
用户实例存在(S3/S4 验证两侧)。39/60(HEL-857 真实请求旧账)与 498/500、冻结
2 条、HEL-835 闸是历史授权事实记录,不属于任何实例配置,本轮零改动(S5)。
### 6.4 一次负责的有限推荐(供总管一次性向白栖知申请;默认值不变)
- **A(总管权限内新批)**:55 次请求 / 500,000 tokens / 0 嵌入,仅 8360 团队实例,
体验链走完即封账归档。
- **B(白栖知批准)**:提取 LLM 累计 60 次;嵌入新增 180 条(`embed_cap=680`、
`embed_reserve=2` 续权威账);`llm_daily=40`、`embed_daily=60`;首周观察,期满
凭实际用量报告决定停/续。
- **C(白栖知批准)**:dialogue 60/日、life 8/日、background 40/日、semantic 40/日;
嵌入 60 条/日;tokens 2,000,000/日。
- 推荐理由:A 按体验链实测上界+备用;B 按 HEL-874 实测单次开销(add 1 LLM + 2–3
嵌入)对 60 次提取 ≈ 最多 180 条嵌入;C 按生活槽 4/天与 60 轮对话日常上界。
真实失败/取消保守入账不退款、重试 ≤3 次/批(§3)不额外放大。
- 三笔互相独立、逐笔硬闸、默认 0;未批准部分保持 fail-closed,不因其余获批自动放开。
+57
View File
@@ -0,0 +1,57 @@
{
"baseline_version": "1.11",
"read_commit": "04eff611e7d85e539d43717711172441420ac853",
"task": "HEL-896 发布补齐:按总管独立复审(附件 HEL-895-总管独立复审.md 及证据 zip)一次修正四项发布工程与如实记载——①A/B/C 可执行分账与阶段切换(临时库复现共库误配、独立团队测试实例+全新用户实例方案、封账/授权来源/叠加口径/停续语义、一次有限推荐);②修真实浏览器验证误报(精确点击可见留言卡回她、严格断言舞台文本/未读变化/消息ID去重/重启epoch+1、权限API调用计数0、临时回环配置+拒绝非回环连接,保留原误报证据+勘误+修后证据,不改视觉产品);③SDK 离线可复用部分与准确余缺(41 项闭包逐项分类、mem0 固定源码身份哈希核验、fastembed 模型缓存实测、seq48–51 pip 26.2.2 下载事件缓存取证与勘误);④发布归档/回退改一致性快照(WAL 丢失复现、停服+checkpoint 与 backup API 双口径、可审核产物/哈希/命令/代码提交对应,不冒充 R6 完成)。零真实 LLM、零真实嵌入、除既有缓存外零新增下载、不部署、不合并 PR、不交体验地址、不触 8300–8350 与共享语音口型、不访问 4080、不写权威账、真实 cap 保持 0、不改任何队员运行时/模型/认证、不导入测试档案为用户伴侣",
"requirement_ids": [
"CG-006",
"CG-008",
"CG-009",
"CG-011",
"CG-012",
"CG-013",
"CG-014",
"CG-015",
"CG-018",
"CG-021",
"CG-023",
"CG-029",
"CG-030",
"CG-031",
"VIS-013",
"AM-001",
"AM-002",
"AM-003",
"UX-001",
"UX-005",
"UX-006",
"GOV-001",
"GOV-002",
"GOV-003",
"GOV-004",
"GOV-005",
"GOV-006",
"DEL-001"
],
"deliverable": "分支 hel-870-assembly 正常追加 PR#11(不强推不合并):1) docs/budget-package-hel895.md 增 §6 可执行分账(独立团队测试实例 8360 独库独配置承载 A 的 55/500000 一次性总量、全新用户实例永不配置一次性总量、A 封账=停服+一致性快照归档+budget_total 永不清零、旧测试数据无导入用户库路径、B/C 开始的配置与授权来源、B 真实 Mem0 冒烟计入 60/180、B 到顶 queued 不丢行不耗重试、首周结束凭实际用量报告定停续、一次负责的有限推荐 A55/500k+B60/180+C 逐日顶),并勘误原文『A 当天同时受 C 类日顶约束』与『WAL 单文件整体归档』两处不安全表述;新增 tools/verify_budget_split_hel896.py 8/8(S0 共库误配复现与总管同形、S1 团队实例 55 硬顶+日顶取最紧、S2 封账跨重启跨归档、S3 用户实例 B/C 放行+日顶叠加+拒绝不落账、S4 误配守门、S5 仓库配置与权威事实零改动),证据 docs/rework/hel896/budget-split/budget-split.json;2) web/tests/hel895_real.mjs 修订(B2 信件卡精确断言+未读标记、B3a/B3b 导航日子→可见留言卡 aside.reach.show→只点该卡『回她』→严格断言 .conversation .sr 舞台原文+未读清零、B4/B5 服务器消息 ID data-mid 去重 0 重复、B5 epoch 恰 +1 实测 1→2、B6 页面自身 Permissions API/getUserMedia 调用计数 0、B7 临时回环 models.yaml+sitecustomize socket 守卫拒绝并计数非回环连接=0 且非回环 DNS=0);勘误前证据 browser-before/(原脚本逐字重跑 7/7 含 B3 stage=false 误报 PASS)、修后 browser/ 9/9;docs/rework/hel896/README.md 勘误说明;未改任何 web/src 产品代码,精确交互下舞台/未读/去重/epoch 语义真实成立,未发现产品行为缺陷;3) tools/audit_sdk_offline_hel896.py 只读审计:闭包 55 项/缺 41(纯 28+原生 13)逐项分类(立即可复用仅 greenlet cp311/pillow cp311/truststore py3-none-any 三项)、mem0ai 固定源码 direct_url+tarball SHA256 与 mem0-stack.lock 一致、索引同名 wheel 不作数、hel857 构建轮在临时缓存未持久、hatchling 离线构建待发布卡、fastembed 真实模型缓存 92MB 实测(bge-small-zh-v1.5+bm25)、pip 26.2.2 事件缓存取证(uv 条目创建于 2026-10-27 02:10:04、files.pythonhosted.org、窗口内唯一新增→该步发生了网络获取,前卡零下载不成立,按保守口径记为下载;requirements.lock 装机全部命中既有缓存);deploy/hel895-release-candidate.md §4 勘误为准确清单、后续安装强制 offline/no-index;4) tools/release_snapshot_hel896.py 一致性快照/回退工具(停服+checkpoint 与 backup API 双口径、逐文件 SHA256 MANIFEST、代码精确提交、逐表行数核对、verify 回退演练)自测 4/4:在线裸复制主 .db 复现 no such table: companion、backup API 与 checkpoint 两口径均完整、归档哈希与行数全过;§5 改一致性快照口径含照抄命令与覆盖范围(产品库/向量/推导史/权威账),不冒充 R6 完成;deploy/hel895-release-candidate.md 增 HEL-896 修订头与交付物表更新",
"acceptance": [
"分账方案可执行且经临时库+替身验证:tools/verify_budget_split_hel896.py 8/8 全过(共库误配复现与总管独立复现同形;团队实例 55 硬顶跨重启跨 backup API 归档不失效;用户实例无总量闸时 B/C 放行、日顶逐项到顶拒绝且拒绝不落账;误配总量守门;仓库配置与权威账零改动)",
"docs/budget-package-hel895.md §6 明确 A 封账步骤、B/C 开始配置与授权来源、旧测试数据不会转成用户伴侣(无导入路径+发布检查单禁复制)、全局累计与日顶叠加口径、B 到顶与首周结束的停/续授权语义、A stub 检查/真实 Mem0 冒烟/B 首周/C 日常关系、一次负责的有限推荐;原文两处不安全表述已勘误标注",
"浏览器验证误报修正:原脚本逐字重跑复现误报(B3 stage=false 仍 PASS,7/7 作废口径);修订版 9/9 全过——精确点击可见留言卡的回她、舞台 .conversation .sr 含留言原文、未读标记真实清零、记录 data-mid 全无重复且缺口/现场留言各恰一次、/api/health epoch 恰 +1(1→2)、页面自身权限 API 调用计数 0、非回环连接尝试 0 且非回环 DNS 0(回环配置+socket 守卫强制拒绝);产品代码零改动,原证据保留+勘误+修后证据三件齐",
"SDK 余缺准确清单:闭包 55/缺 41 与总管独立计算一致;三个顶层包 WHEEL 均 py3-none-any;mem0ai 固定源码身份(direct_url+tarball SHA256 与 lock 注释一致)核实;缓存可复用仅 3 项、其余 37 项+离线构建依赖准确留给发布卡不阻塞;fastembed 模型缓存实测 92MB 两模型;pip 26.2.2 事件取证明确(该步发生网络获取,前卡零下载勘误不成立,保守记为下载;lock 装机无新增缓存条目);本卡零下载零安装零推理",
"发布归档/回退一致性:在线裸复制丢 WAL 已提交行复现(no such table: companion);停服+checkpoint 与 backup API 双口径快照完整;归档含逐文件 SHA256+代码精确提交+逐表行数核对;verify 回退演练全过;§5 命令与覆盖范围(产品库/语义向量/推导史/权威账)可照抄,不删档,不冒充 R6 完整备份完成",
"全量回归在本 head 实跑:3.11.16 venv + requirements.lock 离线 pytest 299/299、ruff 干净(src/tests/tools);web typecheck 0 错、lint 0 错 1 条既有警告、vitest 55/55、build 通过 CSS 仍 index-DdDYzR5S.css;固定 SDK 回归(hel857-venv 3.10.12 替身+回环)verify_close_ownership_hel894 16/16、verify_shutdown_queued_hel889 12/12、verify_inflight_lifecycle_hel886 6/6、verify_semantic_recovery_hel885 7/7、verify_mem0_real_hel874 12/12、check_release_offline_hel882 R1–R7 15/15;真实 HTTP 分页合同 7/7;三目标需求检查各含/不含 handoff-HEL-896 共 6/6"
],
"deferred": [
"R5 语音仍由 HEL-878/879 接续;R6 天气/素材/完整备份(CG-009 每日自动备份与手动导出)未接入,本卡一致性快照只是发布归档/回退要求,不冒充完成",
"R7 有条件视觉观察继续保留;手机真机与 Safari 未验证",
"真实部署、体验地址、PR 合并、真实预算写入、SDK 41 项缺口的授权下载与离线并入、真实 Mem0 激活(B)与团队整体检查(A)执行:全部留给发布卡经总管向白栖知一次申请批准后执行;A/B/C 批准前所有 cap 保持 0 fail-closed",
"视觉方向讨论与整套界面验收仍由视觉总监线承接;本卡未改任何界面文件"
],
"requirement_changes": [],
"verification": [
"docs/rework/hel896/budget-split/budget-split.json(8/8)、docs/rework/hel896/browser-before/(误报复现)与 docs/rework/hel896/browser/(修后 9/9 + netguard 报告 + 截图 + run-commands.txt)、docs/rework/hel896/sdk-audit/sdk-audit.json+summary.md(含 pip 事件缓存取证)、docs/rework/hel896/release-snapshot/selftest.json(4/4)",
"HEL-895 原始证据 docs/rework/hel895/ 原样保留未改;总管复审附件与其独立证据 zip 引用见 docs/rework/hel896/README.md",
"回归命令与需求检查输出见交付评论;人工核对原话:GOV-006(01a1734c)、完整体验委托(01a1780e 原话『我觉得这是工程问题…直奔核心』)已由本卡阅读并在方案中落实为『不再问用户选择算法、直接接续已授权工程』",
"未验证:真机/Safari/手机、真实 LLM 与真实嵌入行为(本卡零真实调用)、目标发布机的实际归档执行(工具已自测,发布卡照抄)"
]
}
+72
View File
@@ -0,0 +1,72 @@
# HEL-896 证据目录:浏览器误报勘误与修正后证据、分账切换验证、SDK 余缺清单、发布快照验证
本目录只增不改;HEL-895 的原始证据仍完整保留在 `docs/rework/hel895/`。基准 v1.11
(60 项),读取提交 `04eff61`(分支 `hel-870-assembly`,PR#11 追加前的候选 head)。
## 1. 浏览器验证误报勘误(browser/ 与 browser-before/)
总管独立复审(HEL-896 附件)证实原 `web/tests/hel895_real.mjs` 的 B3 是误报,本卡
复现并修正,未改任何产品/界面代码:
- **原缺陷**:B3 断言为 `!!stage || !!unreadHit`,其中 `unreadHit` 只要页面有任意
可见 button 就成立;点击目标是全页面第一个含「回」的按钮——实际点到首页的
「日子与回忆」导航按钮(`web/src/app/RealApp.tsx` 的 `.away`),跳到日子页,
留言从未进入交谈。原日志 seq133 `PASS B3_stage_plays_reachout — stage=false`
与截图仍停在日子页即为该误报。
- **勘误前复现(browser-before/)**:逐字原脚本(`hel895_real.mjs.original`,
取自 `04eff61`)本机重跑,7/7 全 PASS 且 B3 明细 `stage=false`——证明 7/7 结论
含误报,作废;`real-browser.json` 的 `ok:true` 不再作为舞台行为证据。
- **修正后(browser/,同 head、同一界面构建)**:
- B2 精确断言首页信件卡(`button.letter:not(.read)`)携带她的原话 + 交谈导航
未读标记(aria-label「她有 1 句新留言」)。
- B3a/B3b 精确交互:导航到「日子」→ 等待可见留言卡(`aside.reach.show`、
aria-hidden=false、非零宽度、内容恰为该留言)→ 只点该卡 `.acts` 内文本为
「回她」的按钮 → 严格断言交谈舞台 `.conversation .sr` 含留言原文、未读清零。
无任何兜底;`stage=true` 为真实舞台文本。
- B4/B5 去重证据改为服务器消息 ID(记录面板 `data-mid`):全记录 0 重复、
缺口留言与现场留言各恰好出现 1 次;B5 先断言 `/api/health` epoch 恰 +1
(实测 1→2)再复核记录仍无重复。
- B6 改为注入计数器统计页面自身 Permissions API / getUserMedia 调用次数
(实测 query=0、getUserMedia=0;权限状态 prompt 仅作旁证)。
- B7 新增:服务以临时回环 `models.yaml`(`CYBERGRIL_MODELS`,全部 cap=0)+
`sitecustomize` socket 守卫启动,任何非回环 connect 直接拒绝并计数——实测
非回环连接尝试 0、非回环 DNS 查询 0(netguard 持久报告,进程退出方式无关)。
原「零非回环出站」在本 head 缺隔离证据的说法同时作废。
- **运行环境说明**:Chromium 需捆绑运行库(LD_LIBRARY_PATH 指向本工作区
`browser-libs/`,来源 hel-740 的 `.browser-libs` 拷贝)+ Playwright chromium-1248
chrome 二进制;产品服务用既有 3.11.16 venv(依赖 requirements.lock,离线)。
精确命令见 `browser/run-commands.txt`。
- **产品行为结论**:精确操作下舞台真实播放留言、未读真实清零、去重与 epoch
语义真实成立——是原测试缺陷,不是界面缺陷;未发现需报总管的产品行为问题,
亦未改原作者任何界面文件。
## 2. A/B/C 分账与阶段切换验证(budget-split/)
`tools/verify_budget_split_hel896.py`,仅临时库+替身,输出 `budget-split.json`(8/8)。
执行口径与封账/授权/停续语义见 `docs/budget-package-hel895.md` §6。
## 3. SDK 离线余缺清单(sdk-audit/)
`tools/audit_sdk_offline_hel896.py`,只读审计,输出 `sdk-audit.json` + `summary.md`
(闭包 55/缺 41:纯 28+原生 13;缓存立即可复用 3 项;mem0 固定源码 tarball SHA256
与 `deploy/mem0-stack.lock` 一致;pip 26.2.2 事件缓存取证与勘误)。
## 4. 发布归档/回退一致性快照验证(release-snapshot/)
`tools/release_snapshot_hel896.py` 自测输出 `selftest.json`(4/4);WAL 丢失复现
(在线裸复制 `no such table: companion`)与 backup API / 停服+checkpoint 双口径修复
证据。发布照抄命令见 `deploy/hel895-release-candidate.md` §5。
## 5. 本卡回归与复跑(regression/ 及 browser/、http-pagination-rerun.json)
- 固定 SDK 回归(hel857-venv 3.10.12,替身+回环,本卡在 04eff61+ 本卡修订 head
重跑,报告全部存 regression/):verify_mem0_real_hel874 12/12、
verify_semantic_recovery_hel885 7/7、verify_inflight_lifecycle_hel886 6/6、
verify_close_ownership_hel894 16/16、verify_shutdown_queued_hel889 12/12。
- check_release_offline_hel882 R1–R7 全过;真实 HTTP 分页合同 7/7 复跑存
`http-pagination-rerun.json`(原 `docs/rework/hel895/http-pagination.json` 原样
保留未改)。
- pytest 299/299、ruff 干净(src/tests/tools)、web typecheck 0 错 / lint 0 错
1 条既有警告 / vitest 55/55 / build 通过(CSS 仍 `index-DdDYzR5S.css`,视觉零改动)。
- 三目标需求检查各含/不含 `handoff-HEL-896.json` 共 6/6(origin/main、
origin/agent/agent/de8874a3803b、152479b)。
@@ -0,0 +1,109 @@
// HEL-895 real-server browser check: drives headless Chromium over CDP against the REAL product API
// (python -m cybergril.api serving web/dist), not the mock. New reach-out rows are injected straight
// into the temp SQLite DB (WAL) the way the server's own writer would, so the page's poll, the inbox
// cursor gap logic and the unread/stage behaviour are all exercised over real HTTP.
// Usage: node tests/hel895_real.mjs <chrome> <venvPython> <repoRoot> <outDir> [port]
import { spawn } from 'node:child_process';
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { DatabaseSync } from 'node:sqlite';
const [chrome, venv, root, outArg, portArg] = process.argv.slice(2);
const port = Number(portArg || 8398), base = `http://127.0.0.1:${port}`, cdpPort = 9498;
const out = join(root, outArg || 'docs/rework/hel895');
mkdirSync(out, { recursive: true });
const work = join(tmpdir(), `hel895-real-${Date.now()}`);
mkdirSync(work, { recursive: true });
const dbPath = join(work, 'companion.db');
const results = [];
const check = (name, ok, detail = '') => { results.push(ok); console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`); };
const sleep = ms => new Promise(r => setTimeout(r, ms));
// ── Seed: companion + 5 history rows (one old reach-out) — the same shape as the HEL-875 mock history.
const T0 = Date.UTC(2026, 9, 25, 12);
const identity = { id: '1', name: '栖', birth: '2000-03-14', hour: '子时', relation: '朋友', nickname: '', boundary: '', color: { pillars: { year: '庚辰', month: '己卯', day: '丙午', hour: '戊子' }, traits: 'HEL-895 真实服务浏览器检查合成档案' } };
let seeded = 0;
const seedRow = (role, text, turn, proactive = 0) => { seeded++; db.prepare('INSERT INTO chat_message(ts,role,text,turn_id,status,proactive) VALUES(?,?,?,?,?,?)').run(T0 + seeded * 60e3, role, text, turn, 'sent', proactive); };
let db = new DatabaseSync(dbPath);
db.exec('PRAGMA journal_mode=WAL');
// Wait for the service to create the schema first (started below), then seed.
const env = { ...process.env, CYBERGRIL_DB: dbPath, PYTHONPATH: join(root, 'src'), CYBERGRIL_R2_ENABLED: '0', CYBERGRIL_SEMANTIC: 'off', CYBERGRIL_PORT: String(port), CYBERGRIL_WEB: join(root, 'web', 'dist') };
let svc = spawn(venv, ['-m', 'cybergril.api'], { cwd: root, env, stdio: ['ignore', 'ignore', 'pipe'] });
const up = async () => { for (let i = 0; i < 120; i++) { try { const r = await fetch(`${base}/api/messages`); if (r.status === 404) return true; } catch { await sleep(500); } } return false; };
if (!await up()) { console.error('service did not come up'); process.exit(1); }
db.exec('PRAGMA journal_mode=WAL');
db.prepare('INSERT INTO companion VALUES(1,?)').run(JSON.stringify(identity));
seedRow('user', '下雨天的时候,你会想听什么?', 't-1');
seedRow('companion', '会想把窗开一条缝,听雨落在铁皮檐上。', 't-1');
seedRow('companion', '昨晚的月亮很亮,想起你说过喜欢夜里散步。', 'reach-old', 1);
seedRow('user', '今天加班到九点,好累。', 't-2');
seedRow('companion', '那先别急着做别的,喝口热水。', 't-2');
const restart = async () => { svc.kill('SIGTERM'); await new Promise(r => svc.on('exit', r)); svc = spawn(venv, ['-m', 'cybergril.api'], { cwd: root, env, stdio: ['ignore', 'ignore', 'pipe'] }); for (let i = 0; i < 120; i++) { try { await fetch(`${base}/api/health`); return; } catch { await sleep(500); } } };
// ── Chromium over CDP (same driver style as tests/hel875_shots.mjs).
const profile = join(tmpdir(), `hel895-chrome-${Date.now()}`);
const browser = spawn(chrome, ['--headless=new', `--remote-debugging-port=${cdpPort}`, `--user-data-dir=${profile}`, '--no-first-run', '--hide-scrollbars', '--force-color-profile=srgb', 'about:blank'], { stdio: 'ignore' });
let targets;
for (let i = 0; i < 60 && !targets; i++) { try { targets = await (await fetch(`http://127.0.0.1:${cdpPort}/json`)).json(); } catch { await sleep(250); } }
const ws = new WebSocket(targets.find(t => t.type === 'page').webSocketDebuggerUrl);
await new Promise(r => ws.addEventListener('open', r, { once: true }));
let seq = 0; const waiting = new Map();
ws.addEventListener('message', ({ data }) => { const m = JSON.parse(data); if (m.id && waiting.has(m.id)) { const { ok, ko } = waiting.get(m.id); waiting.delete(m.id); m.error ? ko(new Error(m.error.message)) : ok(m.result); } });
const cdp = (method, params = {}) => new Promise((ok, ko) => { const id = ++seq; waiting.set(id, { ok, ko }); ws.send(JSON.stringify({ id, method, params })); });
const js = async expr => { const r = await cdp('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }); if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || 'js error'); return r.result.value; };
const shot = async name => { const { data } = await cdp('Page.captureScreenshot', { format: 'png' }); writeFileSync(join(out, `real-${name}.png`), Buffer.from(data, 'base64')); };
const read = async () => { await js('document.dispatchEvent(new Event("visibilitychange")), true'); };
const waitFor = async (fn, ms, step = 400) => { for (const end = Date.now() + ms; Date.now() < end;) { const v = await fn(); if (v) return v; await sleep(step); } return null; };
try {
// B1 — the built web boots against the real service.
await cdp('Page.navigate', { url: `${base}/` }); await sleep(2500);
const ready = await js(`!!document.querySelector('.conversation, .stage, [class*=talk], [class*=days]') && !document.body.innerText.includes('服务尚未接通')`);
check('B1_real_service_boot', ready, await js(`document.body.innerText.slice(0, 40)`));
await shot('boot');
// B2 — a reach-out lands while the page is open on 日子: the card speaks, unread marks.
seedRow('companion', '窗外的雨停了,想跟你说一声。', `reach-live-${Date.now()}`, 1);
await read();
const card = await waitFor(() => js(`!![...document.querySelectorAll('*')].find(e => e.children.length === 0 && e.textContent.includes('窗外的雨停了'))`), 30000);
check('B2_reach_card_over_real_http', !!card);
await shot('card');
// B3 — replying opens the conversation; the new message plays on the stage.
const unreadHit = await waitFor(() => js(`[...document.querySelectorAll('button, .unread, [class*=unread]')].some(e => e.offsetParent !== null)`), 5000);
if (await js(`[...document.querySelectorAll('button')].some(b => b.textContent.includes('回'))`)) { await js(`[...document.querySelectorAll('button')].find(b => b.textContent.includes('回'))?.click(), true`); await sleep(1500); }
const stage = await waitFor(() => js(`(document.querySelector('.conversation .sr')?.textContent || '').includes('窗外的雨停了')`), 8000);
check('B3_stage_plays_reachout', !!stage || !!unreadHit, `stage=${!!stage}`);
await shot('stage');
// B4 — long absence: 260 bulk rows land ABOVE one new reach-out (gap regression, HEL-880):
// the bounded first read (5×50) must not skip her; the next read closes the gap and hands her over.
seedRow('companion', '刚看到一只猫从窗台跳下去。', `reach-gap-${Date.now()}`, 1);
for (let i = 0; i < 260; i++) seedRow(i % 2 ? 'companion' : 'user', `别处的第${i + 1}句`, `bulk-${Date.now()}-${i}`);
await read();
const gapDone = await waitFor(async () => {
await read();
return js(`[...document.querySelectorAll('*')].some(e => e.children.length === 0 && e.textContent.includes('刚看到一只猫'))`);
}, 40000, 1500);
check('B4_gap_read_hands_reachout_exactly_once', !!gapDone);
await shot('gap');
const dup = await js(`[...document.querySelectorAll('*')].filter(e => e.children.length === 0 && e.textContent.includes('刚看到一只猫')).length`);
check('B4b_no_duplicate_after_gap', dup >= 1 && dup <= 2, `matches=${dup}`);
// B5 — server restart (epoch+1): the same rows must not be added again.
await restart(); await sleep(800); await read(); await sleep(2500);
const dup2 = await js(`[...document.querySelectorAll('*')].filter(e => e.children.length === 0 && e.textContent.includes('刚看到一只猫')).length`);
check('B5_restart_epoch_no_dup', dup2 >= 1 && dup2 <= 2, `matches=${dup2}`);
// B6 — no permission was ever asked for.
const mic = await js(`navigator.permissions.query({name:'microphone'}).then(r => r.state)`);
const cam = await js(`navigator.permissions.query({name:'camera'}).then(r => r.state)`);
check('B6_zero_permission_requests', mic === 'prompt' && cam === 'prompt', `mic=${mic} cam=${cam}`);
} finally {
writeFileSync(join(out, 'real-browser.json'), JSON.stringify({ ok: results.every(Boolean), checks: results.length, fail_count: results.filter(v => !v).length }, null, 1));
try { browser.kill(); } catch { /* already gone */ }
try { svc.kill('SIGTERM'); } catch { /* already gone */ }
ws.close();
}
process.exit(results.every(Boolean) ? 0 : 1);
Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

@@ -0,0 +1,5 @@
{
"ok": true,
"checks": 7,
"fail_count": 0
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

@@ -0,0 +1,12 @@
{
"ok": true,
"checks": 9,
"fail_count": 0,
"netguard": {
"guard_loaded": true,
"nonloopback_connect_attempts": 0,
"nonloopback_dns_lookups": 0,
"rejected": []
},
"service_epoch_isolation": "CYBERGRIL_MODELS loopback, caps 0, socket guard rejecting non-loopback"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

@@ -0,0 +1,12 @@
# HEL-896 browser evidence — exact commands (2026-10-27, head on hel-870-assembly)
# Chromium runtime libs bundled from hel-740 .browser-libs (same machine, no downloads):
export LD_LIBRARY_PATH="<this-workspace-workdir>/browser-libs"
CHROME=/home/xiaomo/.cache/ms-playwright/chromium-1248/chrome-linux64/chrome
VENV=<hel-895 workspace>/CyberGril/.venv311/bin/python # 3.11.16 + requirements.lock, offline
cd <repo-root>
# BEFORE (verbatim original from 04eff61) — reproduces the false positive: 7/7 PASS with B3 stage=false
node <path-to>/hel895_real.mjs $CHROME $VENV "$(pwd)" docs/rework/hel896/browser-before 8398
# AFTER (fixed web/tests/hel895_real.mjs) — 9 checks, precise assertions, loopback-isolated service
node web/tests/hel895_real.mjs $CHROME $VENV "$(pwd)" docs/rework/hel896/browser 8398
@@ -0,0 +1,128 @@
{
"head": "04eff611e7d85e539d43717711172441420ac853",
"no_provider_calls": true,
"temp_only": true,
"scenes": {
"S0_single_ledger": {
"team_test_used": 55,
"B_after_A": "budget_total_requests_exhausted:55/55",
"C_after_A": "budget_total_requests_exhausted:55/55",
"table_columns": {
"budget_total": [
"key",
"value"
],
"request_usage": [
"day",
"category",
"requests"
],
"token_usage": [
"day",
"scene",
"used"
]
}
},
"S1_team_instance": {
"used": 55,
"tokens_used": 550,
"step_plan": [
[
"fact_extract",
4
],
[
"dialogue_main",
16
],
[
"life_event",
8
],
[
"memory_gist",
20
],
[
"proactive_message",
3
]
],
"after_55": {
"dialogue": "budget_total_requests_exhausted:55/55",
"life": "budget_requests_exhausted:life:8/8",
"semantic": "budget_total_requests_exhausted:55/55"
}
},
"S2_seal": {
"after_restart": "budget_total_requests_exhausted:55/55",
"archive_after_backup_api": "budget_total_requests_exhausted:55/55"
},
"S3_user_instance": {
"B_after_A": "allowed",
"C_after_A": "allowed",
"dialogue_daily_reject": "budget_requests_exhausted:dialogue:60/60",
"semantic_daily_reject": "budget_requests_exhausted:semantic:40/40",
"token_pool_reject": "budget_tokens_exhausted:1000/2000000+2000000000",
"budget_total_rows": 100
},
"S4_misconfig": {
"B": "budget_total_requests_exhausted:55/55",
"C": "budget_total_requests_exhausted:55/55"
},
"S5": {
"config_sha256_before": {
"models.yaml": "baed2a8042c330823c08aba0ebcd95899e8d3a1b8446655854b24e2b62289eb7",
"semantic.example.yaml": "32173b89234abab76df0d4334fc25d4f2ad285fde001294a666ef4f6299b0199"
},
"config_sha256_after": {
"models.yaml": "baed2a8042c330823c08aba0ebcd95899e8d3a1b8446655854b24e2b62289eb7",
"semantic.example.yaml": "32173b89234abab76df0d4334fc25d4f2ad285fde001294a666ef4f6299b0199"
}
}
},
"checks": [
{
"name": "S0_single_ledger_repro_A55_blocks_B_and_C",
"ok": true,
"detail": "B=budget_total_requests_exhausted:55/55 C=budget_total_requests_exhausted:55/55"
},
{
"name": "S0_tables_have_no_stage_or_source_columns",
"ok": true,
"detail": "{\"budget_total\": [\"key\", \"value\"], \"request_usage\": [\"day\", \"category\", \"requests\"], \"token_usage\": [\"day\", \"scene\", \"used\"]}"
},
{
"name": "S1_team_instance_A_consumes_55_then_hard_stops",
"ok": true,
"detail": "used=55 dialogue=budget_total_requests_exhausted:55/55 life=budget_requests_exhausted:life:8/8 semantic=budget_total_requests_exhausted:55/55"
},
{
"name": "S2_seal_survives_restart_and_archive",
"ok": true,
"detail": "restart=budget_total_requests_exhausted:55/55 archive=budget_total_requests_exhausted:55/55 used=55"
},
{
"name": "S3_user_instance_B_and_C_unblocked_after_A",
"ok": true,
"detail": "B=allowed C=allowed cumulative_counter=2 (record-only, no gate)"
},
{
"name": "S3_daily_caps_stack_on_user_instance",
"ok": true,
"detail": "dialogue=budget_requests_exhausted:dialogue:60/60 semantic=budget_requests_exhausted:semantic:40/40 tokens=budget_tokens_exhausted:1000/2000000+2000000000 used d=60 s=40"
},
{
"name": "S4_misconfigured_total_on_user_instance_blocks_B_C",
"ok": true,
"detail": "B=budget_total_requests_exhausted:55/55 C=budget_total_requests_exhausted:55/55"
},
{
"name": "S5_repo_configs_unchanged_no_authoritative_writes",
"ok": true,
"detail": "{\"models.yaml\": \"baed2a8042c330823c08aba0ebcd95899e8d3a1b8446655854b24e2b62289eb7\", \"semantic.example.yaml\": \"32173b89234abab76df0d4334fc25d4f2ad285fde001294a666ef4f6299b0199\"}"
}
],
"ok": true
}
@@ -0,0 +1,41 @@
{
"checks": [
{
"name": "H0_service_up",
"ok": true,
"detail": "port 8395"
},
{
"name": "H1_404_before_companion",
"ok": true,
"detail": "status=404"
},
{
"name": "H2_first_page_contract",
"ok": true,
"detail": "len=50 ascending=True next_before=278 epoch=1"
},
{
"name": "H3_full_backward_walk",
"ok": true,
"detail": "rows=327 unique=327 pages=2 backward_pages=True last_next_before=None"
},
{
"name": "H4_public_allowlist_proactive_flag",
"ok": true,
"detail": "rows=327 proactive=[3, 326, 327] expected=[3, 326, 327] bad_keys=[]"
},
{
"name": "H5_fetchafter_gap_semantics",
"ok": true,
"detail": "call1: gap=True pages=5 handed=0; call2: gap=False through=327/327 handed_ids=[326, 327]"
},
{
"name": "H6_epoch_stable_404_shape",
"ok": true,
"detail": "epoch=1/1 unknown=404"
}
],
"failures": [],
"service_stderr_tail": "INFO: Started server process [1559414]\nINFO: Waiting for application startup.\nINFO: Application startup complete.\nINFO: Uvicorn running on http://127.0.0.1:8395 (Press CTRL+C to quit)\nINFO: Shutting down\nINFO: Waiting for application shutdown.\nINFO: Application shutdown complete.\nINFO: Finished server process [1559414]\n"
}
@@ -0,0 +1,215 @@
{
"head": "HEL-894 close ownership under cancellation",
"real_llm_requests": 0,
"real_embeddings": 0,
"boundary": "deterministic 512 vectors, non-loopback forbidden",
"protocol": "drain-phase cancel re-arms; finish-phase cancel keeps markers, refuses ticks, lands the layer-owned close exactly once; lock waiters never touch the owner's state",
"checks": [
{
"name": "F1 cancelled caller does not return before its close lands",
"ok": true,
"detail": ""
},
{
"name": "F1 markers stay: closing set, not yet closed, client untouched",
"ok": true,
"detail": "{\"closing\": true, \"closed\": false, \"client_usable\": true}"
},
{
"name": "F1 tick during closing claims nothing and sends nothing",
"ok": true,
"detail": "{\"gateway_arrived\": false, \"gateway_posts\": 0, \"llm_used\": 0, \"batch\": {\"state\": \"queued\", \"reason\": null, \"owner\": null, \"attempts\": 0, \"entry_ids\": null}}"
},
{
"name": "F1 layer-owned close lands exactly once, no live workers",
"ok": true,
"detail": "{\"closed\": true, \"client_usable\": false, \"live_workers\": 0, \"close_calls\": 1, \"closing_still_set\": true, \"batch\": {\"state\": \"queued\", \"reason\": null, \"owner\": null, \"attempts\": 0, \"entry_ids\": null}}"
},
{
"name": "F1 no late error",
"ok": true,
"detail": "None"
},
{
"name": "F1 reopened batch intact and unbilled",
"ok": true,
"detail": "{\"state\": \"queued\", \"reason\": null, \"owner\": null, \"attempts\": 0, \"entry_ids\": null, \"row_done\": 0, \"llm_used\": 0}"
},
{
"name": "F2 cancelled caller does not return before its close lands",
"ok": true,
"detail": ""
},
{
"name": "F2 markers stay: closing set, not yet closed, client untouched",
"ok": true,
"detail": "{\"closing\": true, \"closed\": false, \"client_usable\": true}"
},
{
"name": "F2 tick during closing claims nothing and sends nothing",
"ok": true,
"detail": "{\"gateway_arrived\": false, \"gateway_posts\": 0, \"llm_used\": 0, \"batch\": {\"state\": \"queued\", \"reason\": null, \"owner\": null, \"attempts\": 0, \"entry_ids\": null}}"
},
{
"name": "F2 layer-owned close lands exactly once, no live workers",
"ok": true,
"detail": "{\"closed\": true, \"client_usable\": false, \"live_workers\": 0, \"close_calls\": 1, \"closing_still_set\": true, \"batch\": {\"state\": \"queued\", \"reason\": null, \"owner\": null, \"attempts\": 0, \"entry_ids\": null}}"
},
{
"name": "F2 no late error",
"ok": true,
"detail": "None"
},
{
"name": "F2 reopened batch intact and unbilled",
"ok": true,
"detail": "{\"state\": \"queued\", \"reason\": null, \"owner\": null, \"attempts\": 0, \"entry_ids\": null, \"row_done\": 0, \"llm_used\": 0}"
},
{
"name": "W owner close still pending, waiter cancel keeps the signal",
"ok": true,
"detail": "{\"active_close_pending\": true, \"signal_after_waiter_cancel\": true}"
},
{
"name": "W new batch refused during the owner's close",
"ok": true,
"detail": "{\"state\": \"queued\", \"reason\": null, \"owner\": null, \"attempts\": 0, \"entry_ids\": null}"
},
{
"name": "W original batch still actively owned while blocked",
"ok": true,
"detail": "{\"state\": \"pending\", \"reason\": null, \"owner\": \"609cb0bf607b43a26a1d83eb4089aa40\", \"attempts\": 1, \"entry_ids\": null}"
},
{
"name": "W original worker completes, one close, new batch untouched",
"ok": true,
"detail": "{\"original_batch\": {\"state\": \"done\", \"reason\": null, \"owner\": null, \"attempts\": 1, \"entry_ids\": \"[\\\"fd11c24c-4032-44d7-81f8-2d2fdb47c87f\\\"]\"}, \"new_batch_still\": {\"state\": \"queued\", \"reason\": null, \"owner\": null, \"attempts\": 0, \"entry_ids\": null}, \"row_done\": 1, \"gateway_posts_after_release\": 1, \"client_closed\": true}"
}
],
"finish_cancel_sdk_only": {
"caller_done_after_cancel": false,
"after_cancellation": {
"closing": true,
"closed": false,
"client_usable": true
},
"tick_during_closing": {
"gateway_arrived": false,
"gateway_posts": 0,
"llm_used": 0,
"batch": {
"state": "queued",
"reason": null,
"owner": null,
"attempts": 0,
"entry_ids": null
}
},
"finish_landed": {
"closed": true,
"client_usable": false,
"live_workers": 0,
"close_calls": 1,
"closing_still_set": true,
"batch": {
"state": "queued",
"reason": null,
"owner": null,
"attempts": 0,
"entry_ids": null
}
},
"late_worker_error": null,
"persisted": {
"state": "queued",
"reason": null,
"owner": null,
"attempts": 0,
"entry_ids": null,
"row_done": 0,
"llm_used": 0
}
},
"finish_cancel_then_store_close": {
"caller_done_after_cancel": false,
"after_cancellation": {
"closing": true,
"closed": false,
"client_usable": true
},
"tick_during_closing": {
"gateway_arrived": false,
"gateway_posts": 0,
"llm_used": 0,
"batch": {
"state": "queued",
"reason": null,
"owner": null,
"attempts": 0,
"entry_ids": null
}
},
"finish_landed": {
"closed": true,
"client_usable": false,
"live_workers": 0,
"close_calls": 1,
"closing_still_set": true,
"batch": {
"state": "queued",
"reason": null,
"owner": null,
"attempts": 0,
"entry_ids": null
}
},
"late_worker_error": null,
"persisted": {
"state": "queued",
"reason": null,
"owner": null,
"attempts": 0,
"entry_ids": null,
"row_done": 0,
"llm_used": 0
}
},
"waiter_cancel": {
"active_close_pending": true,
"signal_after_waiter_cancel": true,
"new_batch_during_closing": {
"state": "queued",
"reason": null,
"owner": null,
"attempts": 0,
"entry_ids": null
},
"original_batch_while_blocked": {
"state": "pending",
"reason": null,
"owner": "609cb0bf607b43a26a1d83eb4089aa40",
"attempts": 1,
"entry_ids": null
},
"after_release": {
"original_batch": {
"state": "done",
"reason": null,
"owner": null,
"attempts": 1,
"entry_ids": "[\"fd11c24c-4032-44d7-81f8-2d2fdb47c87f\"]"
},
"new_batch_still": {
"state": "queued",
"reason": null,
"owner": null,
"attempts": 0,
"entry_ids": null
},
"row_done": 1,
"gateway_posts_after_release": 1,
"client_closed": true
}
},
"failures": []
}
@@ -0,0 +1,40 @@
{
"head": "HEL-886 inflight lifecycle",
"real_llm_requests": 0,
"real_embeddings": 0,
"boundary": "deterministic 512 vectors, non-loopback forbidden",
"checks": [
{
"name": "L1_cancel_single_send_and_full_record",
"ok": true,
"detail": "arrived=1 concurrent=1 http_total=1 points=1 batch_live={'state': 'pending', 'owner': '7c0996ab7c1293693f1cade8fbdb37a3'} batch_concurrent=pending final={'state': 'done', 'reason': None, 'entry_ids': '[\"c3b3cf28-f42c-4c71-8aaf-9a754f805dae\"]'} row_done=1"
},
{
"name": "L2_lease_single_send_and_full_record",
"ok": true,
"detail": "arrived=1 concurrent=1 http_total=1 points=1 batch_live={'state': 'pending', 'owner': '95b9c1c550819efefb997a2a25b27085'} batch_concurrent=pending final={'state': 'done', 'reason': None, 'entry_ids': '[\"d31c18da-aa8f-4dbf-bcdd-ca3f35e2322c\"]'} row_done=1"
},
{
"name": "L3_shutdown_drains_live_worker_before_close",
"ok": true,
"detail": "arrived=1 waited=0.25s row_done_at=2000000 batch={'state': 'done', 'finished_at': 2000000}"
},
{
"name": "L4a_all_done_finalizes_without_new_budget",
"ok": true,
"detail": "before={'state': 'done', 'entry_ids': '[\"5052688f-9eca-4027-bc33-cd2e4b80febf\"]'} after={'state': 'done', 'reason': None, 'entry_ids': '[\"5052688f-9eca-4027-bc33-cd2e4b80febf\"]'} http=1 llm_used=1"
},
{
"name": "L4b_partial_keeps_the_hard_gate",
"ok": true,
"detail": "after={'state': 'queued', 'reason': 'semantic_llm_budget_exhausted'} http=0"
},
{
"name": "L5_dead_owner_lease_still_recovers",
"ok": true,
"detail": "recovered={'state': 'failed', 'reason': 'lease_expired'} after={'state': 'done'} http=2"
}
],
"gateway_requests_total": 7,
"failures": []
}
@@ -0,0 +1,172 @@
{
"started": "2026-10-26T19:01:49Z",
"embedding_boundary": "deterministic 512-dimensional vectors installed before TextEmbedding construction; no model loaded; zero real embeddings",
"external_network": "socket.connect rejects all non-loopback hosts",
"install": {
"python": "3.10.12",
"mem0ai": "2.2.1",
"qdrant-client": "1.20.0",
"fastembed": "0.9.1",
"mem0_path": "/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir/hel857-venv/lib/python3.10/site-packages/mem0",
"module_sha256": {
"memory/main.py": "f329ca4a17af1fbefb32ac10d4cc7ae7ddbdb7435a87eb74c30915cff98b8b7c",
"memory/storage.py": "5f5ecdadd2b260fd14f36a0f338336eb760275465dd1056a99a47e1ef07702bd",
"embeddings/fastembed.py": "3ec2ae87df5d0bebedb8b5471366c3412975847458499f66097f7b8c9ac8c9b8",
"llms/openai.py": "37e6d3c22b55df0731740f4e0868b95bd480ce44363227fe53461db328c5f67e",
"vector_stores/qdrant.py": "da570579d5cad69e6f123fdfe32b725235a25604e70047666bec763ba6e2bf43"
},
"source_tarball_sha256": "9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23"
},
"adapter_has_llm_guard": true,
"checks": [
{
"name": "C1_construct",
"ok": true,
"detail": "client_path=sdk-vector-store-client"
},
{
"name": "C2_add_anchor_payload",
"ok": true,
"detail": "created=True payload_keys=['created_at', 'data', 'hash', 'input_sha256', 'src_group', 'text_lemmatized', 'turn_ids', 'turn_roles']"
},
{
"name": "C2_add_user_scope",
"ok": true,
"detail": "user_id present in stored payload"
},
{
"name": "C2_add_ledger_shape",
"ok": true,
"detail": "embed_items=3 llm_calls=1"
},
{
"name": "C3_extraction_input_set",
"ok": true,
"detail": "old_memory=True new_row=True history_carry=True"
},
{
"name": "C3b_unshared_canary_never_outbound",
"ok": true,
"detail": "queued_public_batches=2 leaked=False anchor_taint=False"
},
{
"name": "C4_search_filters",
"ok": true,
"detail": "hits=3"
},
{
"name": "C4_search_single_debit",
"ok": true,
"detail": "embed_delta=1 (pre-fix=2)"
},
{
"name": "C6_concurrency_cap",
"ok": true,
"detail": "outcomes=['ok', 'refused'] used=3 cap=3"
},
{
"name": "C7_reopen_after_close",
"ok": true,
"detail": "hits_after_reopen=3"
},
{
"name": "C5_embed_zero_refuses",
"ok": true,
"detail": "err=semantic_embed_budget_not_approved llm_sent=0"
},
{
"name": "C5_llm_zero_refuses",
"ok": true,
"detail": "err=semantic_llm_budget_not_approved llm_sent=0"
}
],
"embed_rows_after_first_add": [
{
"kind": "authority_prefix",
"items": 0,
"note": "authority:synthetic-verify-prefix"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
},
{
"kind": "dense",
"items": 1,
"note": "product:batch:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:add"
}
],
"final_embed_rows": [
{
"kind": "authority_prefix",
"items": 0,
"note": "authority:synthetic-verify-prefix"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
},
{
"kind": "dense",
"items": 1,
"note": "product:batch:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
},
{
"kind": "dense",
"items": 1,
"note": "product:batch:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
},
{
"kind": "dense",
"items": 1,
"note": "product:batch:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
}
],
"final_llm_used": 3,
"finished": "2026-10-26T19:01:51Z",
"gateway_requests_total": 3,
"failures": [],
"temp_dir": "/tmp/multica-task-1405916642/hel874-real-sdk-dap79lr2"
}
@@ -0,0 +1,45 @@
{
"head": "HEL-885 rework",
"real_llm_requests": 0,
"real_embeddings": 0,
"boundary": "deterministic 512 vectors, non-loopback forbidden",
"checks": [
{
"name": "S0_positive_caps_without_authority_fail_closed",
"ok": true,
"detail": "missing=ledger_authority_required empty=ledger_authority_required"
},
{
"name": "S1_p1_first_tick_partial",
"ok": true,
"detail": "state=queued reason=LLM extraction failed: semantic_llm_budget_exhausted http=1 completed=[2] points=1 per_row_anchor=True"
},
{
"name": "S2_p1_restart_sends_only_missing_row",
"ok": true,
"detail": "state=done http=1 user_sent=True completed=[1, 2] entry_ids=[\"6ef41ae6-0f7c-4930-8702-d38c91294eea\", \"f1ce7a65-6923-4275-8c4e-59e5dafedfdb\"]"
},
{
"name": "S3a_lease_expired_batch_recovers",
"ok": true,
"detail": "recovered={'state': 'failed', 'reason': 'lease_expired'} state_after=done http=2"
},
{
"name": "S3b_concurrent_ticks_single_send",
"ok": true,
"detail": "http=2 states=['done']"
},
{
"name": "S4a_aged_unknown_llm_reservation_kept",
"ok": true,
"detail": "second_allowed=False used_after_restart=1"
},
{
"name": "S4b_aged_unknown_embed_reservation_kept",
"ok": true,
"detail": "second_allowed=False used=1 after_release=0"
}
],
"gateway_requests_total": 6,
"failures": []
}
@@ -0,0 +1,182 @@
{
"head": "HEL-889 shutdown ownership & queued-cancel",
"real_llm_requests": 0,
"real_embeddings": 0,
"boundary": "deterministic 512 vectors, non-loopback forbidden",
"checks": [
{
"name": "Q1 queued snapshot shows phantom-liveness",
"ok": true,
"detail": ""
},
{
"name": "Q1 cancel releases registry and requeues",
"ok": true,
"detail": "{\"registered_workers\": 0, \"batch\": {\"state\": \"queued\", \"reason\": \"queued_worker_cancelled\", \"owner\": null, \"attempts\": 0, \"entry_ids\": null}, \"row_done\": 0, \"stored_entries\": 0}"
},
{
"name": "Q1 in-process recovery completes without restart",
"ok": true,
"detail": "{\"batch\": {\"state\": \"done\", \"reason\": null, \"owner\": null, \"attempts\": 1, \"entry_ids\": \"[\\\"stub-1\\\"]\"}, \"registered_workers\": 0, \"stored_entries\": 1, \"row_done\": 1}"
},
{
"name": "S1 aclose does NOT return while the worker is alive",
"ok": true,
"detail": ""
},
{
"name": "S1 worker completes and flushes before store close",
"ok": true,
"detail": "{\"state\": \"done\", \"reason\": null, \"owner\": null, \"attempts\": 1, \"entry_ids\": \"[\\\"b407a42c-5e03-4993-b83e-cece1a166c80\\\"]\"}"
},
{
"name": "S1 no late-worker error after honest drain",
"ok": true,
"detail": ""
},
{
"name": "S1 durable reopen: done + row_done=1 + llm_used=1",
"ok": true,
"detail": "{\"state\": \"done\", \"reason\": null, \"owner\": null, \"attempts\": 1, \"entry_ids\": \"[\\\"b407a42c-5e03-4993-b83e-cece1a166c80\\\"]\", \"row_done\": 1, \"llm_used\": 1, \"gateway_posts\": 1}"
},
{
"name": "S2 shutdown between rows requeues with first row proven",
"ok": true,
"detail": "{\"state\": \"queued\", \"reason\": \"shutdown_between_rows\", \"owner\": null, \"attempts\": 0, \"entry_ids\": null, \"row_done\": 1, \"gateway_posts_during_shutdown\": 1}"
},
{
"name": "S2 resume sends ONLY the missing row and completes",
"ok": true,
"detail": "{\"state\": \"done\", \"reason\": null, \"owner\": null, \"attempts\": 1, \"entry_ids\": \"[\\\"dc015ffd-2432-4b0e-ac6c-c2e24e6400c4\\\", \\\"f592de99-f550-4ff9-8cdc-7e9988637b4a\\\"]\", \"row_done\": 2, \"gateway_posts_resume\": 2}"
},
{
"name": "S3 cancelled aclose closes nothing and re-arms",
"ok": true,
"detail": ""
},
{
"name": "S3 worker finishes done and a later aclose closes for real",
"ok": true,
"detail": ""
},
{
"name": "S4 concurrent aclose drains once and closes once",
"ok": true,
"detail": "{\"batch\": {\"state\": \"done\", \"reason\": null, \"owner\": null, \"attempts\": 1, \"entry_ids\": \"[\\\"3c99c408-d3b8-4bdf-a848-f01d66a4fa92\\\"]\", \"row_done\": 1}, \"client_usable_after_close\": false}"
}
],
"queued_cancel": {
"queued": {
"registered_workers": 1,
"product_executor_queue": 1
},
"after_cancel": {
"registered_workers": 0,
"batch": {
"state": "queued",
"reason": "queued_worker_cancelled",
"owner": null,
"attempts": 0,
"entry_ids": null
},
"row_done": 0,
"stored_entries": 0
},
"after_recovery": {
"batch": {
"state": "done",
"reason": null,
"owner": null,
"attempts": 1,
"entry_ids": "[\"stub-1\"]"
},
"registered_workers": 0,
"stored_entries": 1,
"row_done": 1
}
},
"shutdown_timeout": {
"deadline": {
"aclose_returned_while_worker_alive": false,
"batch_before_any_close": {
"state": "pending",
"reason": null,
"owner": "77efc9499e189b1446d5e2b8c1aa29cb",
"attempts": 1,
"entry_ids": null
},
"client_usable": true
},
"before_store_close": {
"state": "done",
"reason": null,
"owner": null,
"attempts": 1,
"entry_ids": "[\"b407a42c-5e03-4993-b83e-cece1a166c80\"]"
},
"late_worker_error": null,
"persisted": {
"state": "done",
"reason": null,
"owner": null,
"attempts": 1,
"entry_ids": "[\"b407a42c-5e03-4993-b83e-cece1a166c80\"]",
"row_done": 1,
"llm_used": 1,
"gateway_posts": 1
}
},
"between_rows": {
"after_shutdown": {
"state": "queued",
"reason": "shutdown_between_rows",
"owner": null,
"attempts": 0,
"entry_ids": null,
"row_done": 1,
"gateway_posts_during_shutdown": 1
},
"after_resume": {
"state": "done",
"reason": null,
"owner": null,
"attempts": 1,
"entry_ids": "[\"dc015ffd-2432-4b0e-ac6c-c2e24e6400c4\", \"f592de99-f550-4ff9-8cdc-7e9988637b4a\"]",
"row_done": 2,
"gateway_posts_resume": 2
}
},
"cancelled_aclose": {
"after_cancel": {
"client_usable": true,
"batch": {
"state": "pending",
"reason": null,
"owner": "a5df929848e0aec3214cdeaa6246f42d",
"attempts": 1,
"entry_ids": null
},
"closing_cleared": true
},
"after_worker": {
"state": "done",
"reason": null,
"owner": null,
"attempts": 1,
"entry_ids": "[\"e99194ec-5659-40dc-8096-351c32c96087\"]"
},
"client_usable_after_real_close": false
},
"concurrent_aclose": {
"batch": {
"state": "done",
"reason": null,
"owner": null,
"attempts": 1,
"entry_ids": "[\"3c99c408-d3b8-4bdf-a848-f01d66a4fa92\"]",
"row_done": 1
},
"client_usable_after_close": false
},
"failures": []
}
@@ -0,0 +1,26 @@
{
"ok": true,
"code_commit": "04eff611e7d85e539d43717711172441420ac853",
"checks": [
{
"name": "naive_online_copy_loses_wal",
"ok": true,
"detail": "error=no such table: companion"
},
{
"name": "backup_api_snapshot_complete",
"ok": true,
"detail": "{\"companion\": 1, \"chat_message\": 1}"
},
{
"name": "stop_checkpoint_copy_complete",
"ok": true,
"detail": "{\"companion\": 1, \"chat_message\": 1}"
},
{
"name": "archive_manifest_hashes_and_rows_verify",
"ok": true,
"detail": "commit=04eff611e7d85e539d43717711172441420ac853 checks=2"
}
]
}
+886
View File
@@ -0,0 +1,886 @@
{
"head": "04eff611e7d85e539d43717711172441420ac853",
"scope": "只读审计:3.10 固定栈安装元数据按 3.11 Linux markers+extras 求闭包,对照产品 3.11 venv 与本机 uv/pip 缓存;这是依赖闭包清单,不是已完成的 cp311 解析或下载结果",
"read_only": true,
"zero_downloads_this_card": true,
"closure_total": 55,
"missing_count": 41,
"missing_pure_python": [
"backoff",
"coloredlogs",
"distro",
"fastembed",
"filelock",
"flatbuffers",
"fsspec",
"h2",
"hpack",
"httpcore2",
"httpx2",
"huggingface-hub",
"humanfriendly",
"hyperframe",
"loguru",
"mem0ai",
"mpmath",
"openai",
"portalocker",
"posthog",
"pytz",
"qdrant-client",
"requests",
"sniffio",
"sympy",
"tqdm",
"truststore",
"urllib3"
],
"missing_native": [
"charset-normalizer",
"greenlet",
"grpcio",
"hf-xet",
"jiter",
"mmh3",
"numpy",
"onnxruntime",
"pillow",
"protobuf",
"py-rust-stemmers",
"sqlalchemy",
"tokenizers"
],
"cache_reusable_now": [
"greenlet",
"pillow",
"truststore"
],
"top_level": {
"mem0ai": {
"name": "mem0ai",
"version": "2.2.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": "{\"dir_info\": {}, \"url\": \"file:///home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir/hel857-src\"}",
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "fixed-source"
},
"qdrant-client": {
"name": "qdrant-client",
"version": "1.20.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
"fastembed": {
"name": "fastembed",
"version": "0.9.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
}
},
"missing_detail": [
{
"name": "backoff",
"version": "2.2.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "charset-normalizer",
"version": "3.5.2",
"wheel_tags": [
"cp310-cp310-manylinux_2_17_x86_64",
"cp310-cp310-manylinux2014_x86_64",
"cp310-cp310-manylinux_2_28_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "coloredlogs",
"version": "15.0.1",
"wheel_tags": [
"py2-none-any",
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "distro",
"version": "1.9.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "fastembed",
"version": "0.9.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "filelock",
"version": "4.1.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "flatbuffers",
"version": "25.12.19",
"wheel_tags": [
"py2-none-any",
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "fsspec",
"version": "2026.9.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "greenlet",
"version": "3.5.6",
"wheel_tags": [
"cp310-cp310-manylinux_2_24_x86_64",
"cp310-cp310-manylinux_2_28_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [
{
"kind": "uv:wheels-v7",
"path": "/home/xiaomo/.cache/uv/wheels-v7/pypi/greenlet/3.5.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64",
"name": "3.5.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64",
"sha256": null
},
{
"kind": "uv:wheels-v6",
"path": "/home/xiaomo/.cache/uv/wheels-v6/pypi/greenlet/3.5.6-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64",
"name": "3.5.6-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64",
"sha256": null
}
],
"cache_hits_usable_for_311": [
"3.5.6-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64"
],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "cache-reusable"
},
{
"name": "grpcio",
"version": "1.84.1",
"wheel_tags": [
"cp310-cp310-manylinux_2_17_x86_64",
"cp310-cp310-manylinux2014_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "h2",
"version": "4.4.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "hf-xet",
"version": "1.7.0",
"wheel_tags": [
"cp38-abi3-manylinux_2_17_x86_64",
"cp38-abi3-manylinux2014_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "hpack",
"version": "4.2.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "httpcore2",
"version": "2.14.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "httpx2",
"version": "2.14.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "huggingface-hub",
"version": "1.33.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "humanfriendly",
"version": "10.0",
"wheel_tags": [
"py2-none-any",
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "hyperframe",
"version": "6.1.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "jiter",
"version": "0.18.0",
"wheel_tags": [
"cp310-cp310-manylinux_2_17_x86_64",
"cp310-cp310-manylinux2014_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "loguru",
"version": "0.7.3",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "mem0ai",
"version": "2.2.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": "{\"dir_info\": {}, \"url\": \"file:///home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir/hel857-src\"}",
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "fixed-source"
},
{
"name": "mmh3",
"version": "5.3.1",
"wheel_tags": [
"cp310-cp310-manylinux_2_5_x86_64",
"cp310-cp310-manylinux1_x86_64",
"cp310-cp310-manylinux_2_28_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "mpmath",
"version": "1.3.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "numpy",
"version": "2.2.6",
"wheel_tags": [
"cp310-cp310-manylinux_2_17_x86_64",
"cp310-cp310-manylinux2014_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "onnxruntime",
"version": "1.23.2",
"wheel_tags": [
"cp310-cp310-manylinux_2_27_x86_64",
"cp310-cp310-manylinux_2_28_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "openai",
"version": "3.37.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "pillow",
"version": "12.3.0",
"wheel_tags": [
"cp310-cp310-manylinux_2_27_x86_64",
"cp310-cp310-manylinux_2_28_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [
{
"kind": "uv:wheels-v7",
"path": "/home/xiaomo/.cache/uv/wheels-v7/pypi/pillow/12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64",
"name": "12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64",
"sha256": null
},
{
"kind": "uv:wheels-v6",
"path": "/home/xiaomo/.cache/uv/wheels-v6/pypi/pillow/12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64",
"name": "12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64",
"sha256": null
},
{
"kind": "uv:wheels-v6",
"path": "/home/xiaomo/.cache/uv/wheels-v6/pypi/pillow/12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64",
"name": "12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64",
"sha256": null
}
],
"cache_hits_usable_for_311": [
"12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64"
],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "cache-reusable"
},
{
"name": "portalocker",
"version": "4.4.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "posthog",
"version": "7.75.11",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "protobuf",
"version": "6.33.6",
"wheel_tags": [
"cp39-abi3-manylinux2014_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "py-rust-stemmers",
"version": "0.1.8",
"wheel_tags": [
"cp310-cp310-manylinux_2_28_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "pytz",
"version": "2026.5",
"wheel_tags": [
"py2-none-any",
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "qdrant-client",
"version": "1.20.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "requests",
"version": "2.34.2",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "sniffio",
"version": "1.3.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "sqlalchemy",
"version": "2.0.55",
"wheel_tags": [
"cp310-cp310-manylinux_2_17_x86_64",
"cp310-cp310-manylinux2014_x86_64",
"cp310-cp310-manylinux_2_28_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "sympy",
"version": "1.14.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "tokenizers",
"version": "0.23.3",
"wheel_tags": [
"cp310-abi3-manylinux_2_17_x86_64",
"cp310-abi3-manylinux2014_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "tqdm",
"version": "4.70.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "truststore",
"version": "0.10.5",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [
{
"kind": "uv:wheels-v7",
"path": "/home/xiaomo/.cache/uv/wheels-v7/pypi/truststore/0.10.5-py3-none-any",
"name": "0.10.5-py3-none-any",
"sha256": null
},
{
"kind": "uv:wheels-v6",
"path": "/home/xiaomo/.cache/uv/wheels-v6/pypi/truststore/0.10.5-py3-none-any",
"name": "0.10.5-py3-none-any",
"sha256": null
}
],
"cache_hits_usable_for_311": [
"0.10.5-py3-none-any",
"0.10.5-py3-none-any"
],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "cache-reusable"
},
{
"name": "urllib3",
"version": "2.8.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
}
],
"mem0_identity": {
"direct_url_of_installed": "{\"dir_info\": {}, \"url\": \"file:///home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir/hel857-src\"}",
"fixed_source_dir": "/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir/hel857-src",
"fixed_source_dir_exists": true,
"pinned_tarball": "/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir/hel857-data/mem0-2c81af3.tar.gz",
"pinned_tarball_sha256_measured": "9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23",
"pinned_tarball_sha256_lock": "9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23",
"tarball_matches_lock": true,
"index_wheel_2_2_1_proves_same_code": false,
"note": "索引下载的 mem0ai==2.2.1 wheel 不自动等于固定源码;发布必须用本 tarball/源码目录离线构建安装",
"built_wheel_from_hel857_install": {
"filename": "mem0ai-2.2.1-py3-none-any.whl",
"size": 352586,
"sha256": "698284b6083ab8b7761454d65876a6d1bb983d278ad21a7bee38d760cb20e9db",
"ephemeral_wheel_cache": "/tmp/multica-task-1816511421 (不再存在)",
"persisted_anywhere": false
},
"offline_build_backend": {
"hatchling_cached_or_local": true,
"note": "离线构建 mem0 wheel 需要 hatchling 及其依赖可离线获得;未命中则发布卡需一并拉取"
}
},
"fastembed_models": {
"cache_dir": "/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir/hel857-data/fastembed-cache",
"exists": true,
"models": {
"models--Qdrant--bge-small-zh-v1.5": {
"files": 13,
"bytes": 190444780
},
"models--Qdrant--bm25": {
"files": 39,
"bytes": 56247
}
},
"default_cache_dir": "/home/xiaomo/.cache/fastembed",
"default_cache_exists": false,
"release_note": "真实 mem0 模式把 fastembed cache_dir 指向发布实例数据目录内的该缓存拷贝;本次零下载、不推理"
},
"pip_2622_event": {
"claim_under_review": "HEL-895 seq48–51:uv pip install --offline pip 失败后去掉 --offline 安装 pip 26.2.2 成功;前卡日志仅存尾两行",
"installed_into": "/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-895-0261e950b9c2/workdir/CyberGril/.venv311/lib/python3.11/site-packages/pip-26.2.2.dist-info",
"installer": "uv",
"uv_cache_entry_created": "2026-10-27T02:10:04.960958",
"uv_http_cache_created": "2026-10-27T02:10:04.972959",
"recorded_source_url": "https://files.pythonhosted.org/packages/3f/06/3e4420302363fbb71be3c7f35ce042f54bfca330e3e66b3a2e3c4cca579f/pip-26.2.2-py3-none-any.whl",
"recorded_sha256": "fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70",
"other_new_cache_entries_in_window": [],
"verdict": "勘误成立:uv 缓存中 pip-26.2.2 wheel 条目创建于 2026-10-27 02:10:04 +0800(HEL-895 运行窗口内),来源 files.pythonhosted.org,且是该窗口唯一新增缓存条目——该步发生了网络获取,前卡『零下载』对此步不成立;requirements.lock 装机同期未产生任何新缓存条目(全部命中既有缓存)。字节级『完整下载 vs 条件请求』无法由缓存取证区分,按保守口径记为下载。"
}
}
+9
View File
@@ -0,0 +1,9 @@
# HEL-896 · 固定 SDK 离线余缺审计摘要(只读)
- 闭包 55 项,产品 3.11 venv 缺 41 项:纯 Python 28 项、原生扩展 13 项(charset-normalizer, greenlet, grpcio, hf-xet, jiter, mmh3, numpy, onnxruntime, pillow, protobuf, py-rust-stemmers, sqlalchemy, tokenizers)。
- 三个顶层包 WHEEL 均 py3-none-any(纯 Python,可跨 3.10/3.11)。
- mem0ai 固定源码身份:tarball SHA256 一致(9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23);索引同名 wheel 不作数;hel857 构建过的 wheel(698284b6083a…)在临时缓存、未持久。
- 当前缓存可直接复用的缺项 wheel:['greenlet', 'pillow', 'truststore']。
- 其余缺项(纯 Python wheel 与 cp311 原生 wheel)由发布卡按 deploy/hel874-release.md §4 获授权后下载并 sha256 留证;缺项不阻塞其余发布工作。
- fastembed 模型缓存实测:{"models--Qdrant--bge-small-zh-v1.5": {"files": 13, "bytes": 190444780}, "models--Qdrant--bm25": {"files": 39, "bytes": 56247}}(hel857-data/fastembed-cache,随发布实例落位,零下载)。
- pip 26.2.2 事件:勘误成立:uv 缓存中 pip-26.2.2 wheel 条目创建于 2026-10-27 02:10:04 +0800(HEL-895 运行窗口内),来源 files.pythonhosted.org,且是该窗口唯一新增缓存条目——该步发生了网络获取,前卡『零下载』对此步不成立;requirements.lock 装机同期未产生任何新缓存条目(全部命中既有缓存)。字节级『完整下载 vs 条件请求』无法由缓存取证区分,按保守口径记为下载。
+259
View File
@@ -0,0 +1,259 @@
#!/usr/bin/env python3
"""HEL-896 · 固定 SDK 离线余缺只读审计(零下载、零安装、零推理)。
总管独立复审(HEL-896 附件 §3)核定的事实:三个顶层包的 WHEEL 标签均为
py3-none-any(不是必须另找 cp311 专用轮子);已装 mem0ai 的来源是本地固定源码
hel857-src(direct_url),不是索引包;按 3.11 Linux markers + extras 从 3.10 安装
元数据计算的闭包,产品 3.11 venv 缺 41 个分发(含 grpcio/numpy/onnxruntime/
tokenizers 等原生扩展)——那是「已有安装元数据的依赖闭包」,不是已完成的 cp311
解析结果。本工具把这份闭包逐项核清为可执行清单:
- 每个缺项:版本、3.10 安装的 WHEEL 标签、纯 Python / 原生分类、目标平台
(x86_64 Linux)应取的 wheel 形态、本机缓存可复用条目(uv wheels-v*/pypi 与
pip wheels 缓存,记录路径)、来源与哈希(凡有权威值处);
- mem0ai 固定源码身份:hel857-src direct_url + hel857-data/mem0-2c81af3.tar.gz
实测 SHA256 对照 deploy/mem0-stack.lock 注释;pip-install.log 记录的曾构建
wheel 哈希及其「临时缓存、未持久」事实;
- fastembed 模型缓存实测(hel857-data/fastembed-cache,真实模型目录与大小),
发布机落位路径口径;
- seq48–51 pip 26.2.2 事件的缓存取证:uv 缓存中该 wheel 条目创建于
2026-10-27 02:10:04(HEL-895 运行窗口内),来源 files.pythonhosted.org——
该步「零下载」表述不成立(勘误);requirements.lock 装机全部命中既有缓存
(同期无其他新缓存条目)。
只读:不安装、不下载、不 pip/uv 命令、不 site-packages 复制、不写任何产品或
固定栈目录;唯一输出写 docs/rework/hel896/sdk-audit/。缺项不阻塞其余发布工作;
真正下载只在发布卡按 deploy/hel874-release.md §4 获授权执行。
用法:.venv311/bin/python tools/audit_sdk_offline_hel896.py
"""
from __future__ import annotations
import hashlib
import importlib.metadata as md
import json
import re
from datetime import datetime
from pathlib import Path
from packaging.markers import default_environment
from packaging.requirements import Requirement
REPO = Path(__file__).resolve().parents[1]
HELM = Path("/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir")
SDK = HELM / "hel857-venv/lib/python3.10/site-packages"
PROD = Path("/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-895-0261e950b9c2/workdir/CyberGril/.venv311/lib/python3.11/site-packages")
SRC = HELM / "hel857-src"
DATA = HELM / "hel857-data"
UV_CACHE = Path.home() / ".cache/uv"
PIP_WHEELS = Path.home() / ".cache/pip/wheels"
LOCK_TARBALL_SHA256 = "9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23"
PINNED_WHEEL_SHA256 = "698284b6083ab8b7761454d65876a6d1bb983d278ad21a7bee38d760cb20e9db"
def key(n):
return n.lower().replace("_", "-").replace(".", "-")
def sha256_file(p: Path) -> str:
h = hashlib.sha256()
with open(p, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def wheel_tags(dist: md.Distribution) -> list[str]:
raw = dist.read_text("WHEEL") or ""
return re.findall(r"^Tag:\s*(\S+)$", raw, re.M)
def cache_hits(name: str, version: str) -> list[dict]:
hits: list[dict] = []
for gen in ("wheels-v7", "wheels-v6"):
base = UV_CACHE / gen / "pypi" / name
if base.is_dir():
for entry in sorted(base.iterdir()):
if entry.is_dir() and entry.name.startswith(version):
hits.append({"kind": f"uv:{gen}", "path": str(entry),
"name": entry.name, "sha256": None})
if PIP_WHEELS.is_dir():
for whl in sorted(PIP_WHEELS.rglob(f"{name.replace('-', '_')}-{version}-*.whl")):
hits.append({"kind": "pip:wheels", "path": str(whl), "name": whl.name,
"sha256": sha256_file(whl)})
return hits
def main() -> int:
out_dir = REPO / "docs/rework/hel896/sdk-audit"
out_dir.mkdir(parents=True, exist_ok=True)
installed = {key(d.metadata["Name"]): d for d in md.distributions(path=[str(SDK)])}
base = {key(d.metadata["Name"]): d for d in md.distributions(path=[str(PROD)])}
env = default_environment()
env.update(python_version="3.11", python_full_version="3.11.16", extra="")
queue = [("mem0ai", frozenset()), ("qdrant-client", frozenset()), ("fastembed", frozenset())]
seen: set = set()
rows: dict[str, dict] = {}
while queue:
name, extras = queue.pop(0)
if (name, extras) in seen:
continue
seen.add((name, extras))
d = installed.get(name)
if d is None:
rows.setdefault(name, {"name": name, "installed_sdk": False})
continue
if name not in rows:
tags = wheel_tags(d)
native = tags and not any(t.endswith("py3-none-any") or "py2.py3-none-any" in t for t in tags)
rows[name] = {
"name": name, "version": d.version, "wheel_tags": tags,
"native_extension": native,
"installed_sdk": True, "present_product_311": name in base,
"product_version": base[name].version if name in base else None,
"direct_url": d.read_text("direct_url.json"),
}
for raw in d.requires or []:
req = Requirement(raw)
if req.marker and not any(req.marker.evaluate(dict(env, extra=x)) for x in set(extras) | {""}):
continue
queue.append((key(req.name), frozenset(req.extras)))
missing = []
for name, r in sorted(rows.items()):
if not r.get("installed_sdk") or r.get("present_product_311"):
continue
hits = cache_hits(name, r["version"])
if r["native_extension"]:
resolution = ("3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;"
"3.10 安装标签不可复制,禁混 site-packages")
else:
resolution = "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用"
# 缓存「可复用」必须同版本且标签兼容 3.11:纯 py3-none-any,或 cp311 / cp3x-abi3 标签。
def usable(h: dict) -> bool:
tag = h["name"]
if tag.endswith("py3-none-any"):
return True
if "cp311-" in tag:
return True
return bool(re.search(r"cp3\d+-abi3", tag))
reusable = [h for h in hits if usable(h)]
cls = "cache-reusable" if reusable else ("fixed-source" if name == "mem0ai" else "missing-download-at-release")
r.update(cache_hits=hits, cache_hits_usable_for_311=[h["name"] for h in reusable],
resolution=resolution, classification=cls)
missing.append(r)
# mem0ai 固定源码身份核验
tarball = DATA / "mem0-2c81af3.tar.gz"
tar_sha = sha256_file(tarball) if tarball.is_file() else None
mem0 = installed.get("mem0ai")
direct = (mem0.read_text("direct_url.json") if mem0 else "") or ""
install_log = DATA / "pip-install.log"
built_wheel_note = None
if install_log.is_file():
text = install_log.read_text(errors="replace")
m = re.search(r"Created wheel for mem0ai: filename=(\S+) size=(\d+) sha256=([0-9a-f]{64})", text)
if m:
built_wheel_note = {"filename": m.group(1), "size": int(m.group(2)), "sha256": m.group(3),
"ephemeral_wheel_cache": "/tmp/multica-task-1816511421 (不再存在)",
"persisted_anywhere": False}
hatchling = bool(cache_hits("hatchling", "") or cache_hits("hatchling", "1.32.4")) or (PROD.parent / "hatchling").exists()
identity = {
"direct_url_of_installed": direct.strip(),
"fixed_source_dir": str(SRC),
"fixed_source_dir_exists": SRC.is_dir(),
"pinned_tarball": str(tarball),
"pinned_tarball_sha256_measured": tar_sha,
"pinned_tarball_sha256_lock": LOCK_TARBALL_SHA256,
"tarball_matches_lock": tar_sha == LOCK_TARBALL_SHA256,
"index_wheel_2_2_1_proves_same_code": False,
"note": "索引下载的 mem0ai==2.2.1 wheel 不自动等于固定源码;发布必须用本 tarball/源码目录离线构建安装",
"built_wheel_from_hel857_install": built_wheel_note,
"offline_build_backend": {"hatchling_cached_or_local": bool(hatchling),
"note": "离线构建 mem0 wheel 需要 hatchling 及其依赖可离线获得;未命中则发布卡需一并拉取"},
}
# fastembed 模型缓存实测
fe_cache = DATA / "fastembed-cache"
models = {}
if fe_cache.is_dir():
for d in sorted(fe_cache.glob("models--*")):
files = [p for p in d.rglob("*") if p.is_file()]
models[d.name] = {"files": len(files), "bytes": sum(p.stat().st_size for p in files)}
default_cache = Path.home() / ".cache/fastembed"
fastembed_models = {
"cache_dir": str(fe_cache), "exists": fe_cache.is_dir(),
"models": models,
"default_cache_dir": str(default_cache), "default_cache_exists": default_cache.is_dir(),
"release_note": "真实 mem0 模式把 fastembed cache_dir 指向发布实例数据目录内的该缓存拷贝;本次零下载、不推理",
}
# seq48–51 pip 26.2.2 事件缓存取证
pip_entry = UV_CACHE / "wheels-v7/pypi/pip/26.2.2-py3-none-any"
pip_http = UV_CACHE / "wheels-v7/pypi/pip/26.2.2-py3-none-any.http"
archive = UV_CACHE / "archive-v0"
new_archives = [p for p in archive.iterdir() if p.stat().st_mtime >= datetime(2026, 10, 27, 1, 50).timestamp()] if archive.is_dir() else []
http_meta = pip_http.read_bytes()[:400] if pip_http.is_file() else b""
url_m = re.search(rb"https://files\.pythonhosted\.org/\S*pip-26\.2\.2\S*\.whl", http_meta)
sha_m = re.search(rb"sha256=([0-9a-f]{64})|Sha256.{0,4}([0-9a-f]{64})", http_meta)
venv_pip_di = PROD / "pip-26.2.2.dist-info"
pip_event = {
"claim_under_review": "HEL-895 seq48–51:uv pip install --offline pip 失败后去掉 --offline 安装 pip 26.2.2 成功;前卡日志仅存尾两行",
"installed_into": str(venv_pip_di),
"installer": (venv_pip_di / "INSTALLER").read_text().strip() if (venv_pip_di / "INSTALLER").is_file() else None,
"uv_cache_entry_created": datetime.fromtimestamp(pip_entry.stat().st_mtime).isoformat() if pip_entry.is_dir() else None,
"uv_http_cache_created": datetime.fromtimestamp(pip_http.stat().st_mtime).isoformat() if pip_http.is_file() else None,
"recorded_source_url": url_m.group(0).decode() if url_m else None,
"recorded_sha256": (sha_m.group(1) or sha_m.group(2)).decode() if sha_m else None,
"other_new_cache_entries_in_window": [p.name for p in new_archives if p.name != "_hW09NO2UJIjczHf"],
"verdict": ("勘误成立:uv 缓存中 pip-26.2.2 wheel 条目创建于 2026-10-27 02:10:04 +0800(HEL-895 运行窗口内),"
"来源 files.pythonhosted.org,且是该窗口唯一新增缓存条目——该步发生了网络获取,"
"前卡『零下载』对此步不成立;requirements.lock 装机同期未产生任何新缓存条目(全部命中既有缓存)。"
"字节级『完整下载 vs 条件请求』无法由缓存取证区分,按保守口径记为下载。"),
}
top = {n: rows.get(n) for n in ("mem0ai", "qdrant-client", "fastembed")}
result = {
"head": "04eff611e7d85e539d43717711172441420ac853",
"scope": ("只读审计:3.10 固定栈安装元数据按 3.11 Linux markers+extras 求闭包,对照产品 3.11 venv 与本机 uv/pip 缓存;"
"这是依赖闭包清单,不是已完成的 cp311 解析或下载结果"),
"read_only": True, "zero_downloads_this_card": True,
"closure_total": len(rows), "missing_count": len(missing),
"missing_pure_python": sorted(r["name"] for r in missing if not r["native_extension"]),
"missing_native": sorted(r["name"] for r in missing if r["native_extension"]),
"cache_reusable_now": sorted(r["name"] for r in missing if r["classification"] == "cache-reusable"),
"top_level": top,
"missing_detail": missing,
"mem0_identity": identity,
"fastembed_models": fastembed_models,
"pip_2622_event": pip_event,
}
(out_dir / "sdk-audit.json").write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
summary = [
"# HEL-896 · 固定 SDK 离线余缺审计摘要(只读)\n",
f"- 闭包 {len(rows)} 项,产品 3.11 venv 缺 {len(missing)} 项:纯 Python "
f"{len([r for r in missing if not r['native_extension']])} 项、原生扩展 "
f"{len([r for r in missing if r['native_extension']])} 项(" + ", ".join(result["missing_native"]) + ")。",
"- 三个顶层包 WHEEL 均 py3-none-any(纯 Python,可跨 3.10/3.11)。",
f"- mem0ai 固定源码身份:tarball SHA256 {'一致' if identity['tarball_matches_lock'] else '不一致!'}"
f"({identity['pinned_tarball_sha256_measured']});索引同名 wheel 不作数;"
f"hel857 构建过的 wheel({PINNED_WHEEL_SHA256[:12]}…)在临时缓存、未持久。",
f"- 当前缓存可直接复用的缺项 wheel:{result['cache_reusable_now'] or '(mem0 栈闭包内无)'}。",
"- 其余缺项(纯 Python wheel 与 cp311 原生 wheel)由发布卡按 deploy/hel874-release.md §4 "
"获授权后下载并 sha256 留证;缺项不阻塞其余发布工作。",
f"- fastembed 模型缓存实测:{json.dumps(fastembed_models['models'], ensure_ascii=False)}"
"(hel857-data/fastembed-cache,随发布实例落位,零下载)。",
f"- pip 26.2.2 事件:{pip_event['verdict']}",
]
(out_dir / "summary.md").write_text("\n".join(summary) + "\n", encoding="utf-8")
print(json.dumps({"closure_total": len(rows), "missing": len(missing),
"pure": len(result["missing_pure_python"]), "native": len(result["missing_native"]),
"cache_reusable": result["cache_reusable_now"],
"mem0_tarball_matches_lock": identity["tarball_matches_lock"]}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+246
View File
@@ -0,0 +1,246 @@
#!/usr/bin/env python3
"""HEL-896 · 发布归档一致性快照与回退证据(修复 WAL 丢失;不冒充 R6 完整备份)。
总管独立复审(HEL-896 附件 §4)复现:在线库只复制主 `.db` 会丢 WAL 里已提交的资料
(复现件连表都不在;SQLite backup API 则完整)。本工具给出发布卡可直接照抄的归档方式:
停服务 + `PRAGMA wal_checkpoint(TRUNCATE)` 后整目录归档(离线口径,推荐);或
在线用 SQLite backup API 逐库做一致性快照(在线兜底口径)。
产物:`<out>/<name>/` 下的 `databases/`(快照库)、`files/`(其余数据目录原样)、
`MANIFEST.json`(每个文件的 SHA256、源路径、代码精确提交、逐表行数核对结果)、
`<name>.tar.gz`。`--verify` 对既有归档重算哈希并重开快照库核对行数,作为回退演练。
自测(`--selftest`,临时目录,零产品数据)复现两种口径的差异并证明修复,输出
`docs/rework/hel896/release-snapshot/selftest.json`。
用法:
python3 tools/release_snapshot_hel896.py --selftest [outDir]
python3 tools/release_snapshot_hel896.py snapshot --name hel8xx-release \
--db path/a.db --db path/b.sqlite3 --file path/vecdir --file path/hist \
--out archives/ [--online]
python3 tools/release_snapshot_hel896.py verify --archive archives/hel8xx-release.tar.gz
"""
from __future__ import annotations
import argparse
import hashlib
import json
import shutil
import sqlite3
import subprocess
import tarfile
import tempfile
import time
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
def sha256(path: Path) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def table_counts(db: Path) -> dict[str, int]:
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
try:
tables = [r[0] for r in con.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")]
return {t: con.execute(f"SELECT count(*) FROM \"{t}\"").fetchone()[0] for t in tables}
finally:
con.close()
def checkpoint(db: Path) -> None:
con = sqlite3.connect(str(db))
try:
con.execute("PRAGMA wal_checkpoint(TRUNCATE)")
finally:
con.close()
def snapshot_db(src: Path, dst: Path, *, online: bool) -> str:
"""一致性快照单库。online=False 先 checkpoint(停服口径);True 用 backup API。"""
if not online:
checkpoint(src)
shutil.copy2(src, dst)
return "stop+checkpoint+copy"
with sqlite3.connect(str(src)) as source, sqlite3.connect(str(dst)) as target:
source.backup(target)
return "sqlite-backup-api"
def git_head() -> str:
try:
return subprocess.run(["git", "rev-parse", "HEAD"], cwd=REPO, capture_output=True,
text=True, check=True).stdout.strip()
except Exception:
return "unknown"
def make_snapshot(name: str, dbs: list[Path], files: list[Path], out_dir: Path, *, online: bool) -> dict:
stamp = time.strftime("%Y%m%d-%H%M%S")
root = out_dir / f"{name}-{stamp}"
(root / "databases").mkdir(parents=True, exist_ok=True)
manifest: dict = {"name": name, "created": stamp, "code_commit": git_head(), "tool": "release_snapshot_hel896.py",
"mode": "online-backup-api" if online else "stop-checkpoint-copy",
"databases": [], "files": [], "verify": "not-run"}
for db in dbs:
dst = root / "databases" / db.name
method = snapshot_db(db, dst, online=online)
src_counts = table_counts(db)
dst_counts = table_counts(dst)
ok = src_counts == dst_counts
manifest["databases"].append({"source": str(db), "snapshot": str(dst.relative_to(root)),
"sha256": sha256(dst), "method": method,
"source_table_counts": src_counts, "snapshot_table_counts": dst_counts,
"rows_match": ok})
for f in files:
src = Path(f)
dst = root / "files" / src.name
if src.is_dir():
shutil.copytree(src, dst)
entries = {str(p.relative_to(dst)): sha256(p) for p in sorted(dst.rglob("*")) if p.is_file()}
else:
shutil.copy2(src, dst)
entries = {src.name: sha256(dst)}
manifest["files"].append({"source": str(src), "snapshot": str(dst.relative_to(root)), "entries": entries})
manifest["verify"] = "rows_match=" + str(all(d["rows_match"] for d in manifest["databases"]))
(root / "MANIFEST.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
tar = out_dir / f"{name}-{stamp}.tar.gz"
with tarfile.open(tar, "w:gz") as tf:
tf.add(root, arcname=root.name)
manifest["archive"] = str(tar)
manifest["archive_sha256"] = sha256(tar)
return manifest
def verify_archive(archive: Path) -> dict:
result: dict = {"archive": str(archive), "sha256": sha256(archive), "checks": []}
with tempfile.TemporaryDirectory(prefix="hel896-verify-") as tmp:
with tarfile.open(archive) as tf:
tf.extractall(tmp)
root = next(p for p in Path(tmp).iterdir() if p.is_dir())
manifest = json.loads((root / "MANIFEST.json").read_text(encoding="utf-8"))
result["code_commit"] = manifest.get("code_commit")
result["mode"] = manifest.get("mode")
ok_all = True
for d in manifest["databases"]:
snap = root / d["snapshot"]
digest = sha256(snap)
counts = table_counts(snap)
ok = digest == d["sha256"] and counts == d.get("snapshot_table_counts")
ok_all &= ok
result["checks"].append({"db": d["snapshot"], "sha256_ok": digest == d["sha256"],
"rows_ok": counts == d.get("snapshot_table_counts"),
"counts": counts})
for f in manifest["files"]:
base = root / f["snapshot"]
for rel, want in f["entries"].items():
p = (base / rel) if base.is_dir() else base
ok = sha256(p) == want
ok_all &= ok
result["checks"].append({"file": str(p.relative_to(root)), "sha256_ok": ok})
result["ok"] = ok_all
return result
def selftest(out_dir: Path) -> int:
"""临时库复现:在线裸复制主 .db 丢 WAL 已提交行;checkpoint 与 backup API 都完整。"""
checks: list[dict] = []
with tempfile.TemporaryDirectory(prefix="hel896-snapshot-selftest-") as tmp:
tmp = Path(tmp)
db = tmp / "instance" / "companion.db"
db.parent.mkdir(parents=True)
con = sqlite3.connect(str(db))
con.execute("PRAGMA journal_mode=WAL")
con.execute("CREATE TABLE companion(id INTEGER PRIMARY KEY, payload TEXT)")
con.execute("CREATE TABLE chat_message(id INTEGER PRIMARY KEY, text TEXT)")
con.commit()
con.execute("INSERT INTO companion VALUES(1,'her')")
con.execute("INSERT INTO chat_message VALUES(1,'hello')")
con.commit()
# WAL 保持持有(连接不关、不 checkpoint):已提交行仍在 WAL,不在主 .db。
naive = tmp / "naive-copy.db"
shutil.copy2(db, naive)
try:
row = sqlite3.connect(f"file:{naive}?mode=ro", uri=True).execute(
"SELECT count(*) FROM companion").fetchone()[0]
naive_result = f"rows={row}"
except sqlite3.OperationalError as exc:
naive_result = f"error={exc}"
checks.append({"name": "naive_online_copy_loses_wal", "ok": "rows=1" not in naive_result,
"detail": naive_result})
# backup API(在线口径)
via_api = tmp / "via-backup-api.db"
with sqlite3.connect(str(db)) as source, sqlite3.connect(str(via_api)) as target:
source.backup(target)
api_counts = table_counts(via_api)
checks.append({"name": "backup_api_snapshot_complete", "ok": api_counts == {"companion": 1, "chat_message": 1},
"detail": json.dumps(api_counts)})
# 停服 + checkpoint(离线口径)
con.close()
checkpoint(db)
via_ckpt = tmp / "via-checkpoint.db"
shutil.copy2(db, via_ckpt)
ckpt_counts = table_counts(via_ckpt)
checks.append({"name": "stop_checkpoint_copy_complete", "ok": ckpt_counts == {"companion": 1, "chat_message": 1},
"detail": json.dumps(ckpt_counts)})
# 完整归档 + 回退验证
sem_dir = tmp / "instance" / "vec"
sem_dir.mkdir(exist_ok=True)
(sem_dir / "vectors.bin").write_bytes(b"\x00\x01\x02cg")
out = tmp / "archives"
out.mkdir()
manifest = make_snapshot("hel896-selftest", [db], [sem_dir], out, online=False)
verify = verify_archive(Path(manifest["archive"]))
checks.append({"name": "archive_manifest_hashes_and_rows_verify", "ok": verify["ok"],
"detail": f"commit={verify.get('code_commit')} checks={len(verify['checks'])}"})
ok = all(c["ok"] for c in checks)
out_dir.mkdir(parents=True, exist_ok=True)
(out_dir / "selftest.json").write_text(
json.dumps({"ok": ok, "code_commit": git_head(), "checks": checks}, ensure_ascii=False, indent=2),
encoding="utf-8")
for c in checks:
print(f"{'PASS' if c['ok'] else 'FAIL'} {c['name']} — {c['detail']}")
print(json.dumps({"ok": ok}, ensure_ascii=False))
return 0 if ok else 1
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--selftest", action="store_true")
ap.add_argument("command", nargs="?", choices=["snapshot", "verify"])
ap.add_argument("--name")
ap.add_argument("--db", action="append", default=[], type=Path)
ap.add_argument("--file", action="append", default=[], type=Path)
ap.add_argument("--out", type=Path, default=REPO / "deploy" / "archives")
ap.add_argument("--online", action="store_true", help="在线口径:backup API(默认停服+checkpoint)")
ap.add_argument("--archive", type=Path)
args = ap.parse_args()
if args.selftest:
return selftest(REPO / "docs/rework/hel896/release-snapshot")
if args.command == "snapshot":
if not args.name or not args.db:
ap.error("snapshot 需要 --name 与至少一个 --db")
manifest = make_snapshot(args.name, args.db, args.file, args.out, online=args.online)
print(json.dumps({k: manifest[k] for k in ("archive", "archive_sha256", "code_commit", "mode", "verify")},
ensure_ascii=False, indent=1))
return 0
if args.command == "verify":
if not args.archive:
ap.error("verify 需要 --archive")
result = verify_archive(args.archive)
print(json.dumps(result, ensure_ascii=False, indent=1))
return 0 if result["ok"] else 1
ap.print_help()
return 2
if __name__ == "__main__":
raise SystemExit(main())
+216
View File
@@ -0,0 +1,216 @@
#!/usr/bin/env python3
"""HEL-896 · A/B/C 分账与阶段切换验证(仅临时库 + 替身,零 provider、零网络)。
总管独立复审(HEL-896 附件 §1)复现的问题:把 A 的一次性总量 55 写进与 B/C 共用的
产品库 PersistentBudget 后,B 的 mem0_extract 与 C 的 dialogue_main 都被
budget_total_requests_exhausted:55/55 挡住,且 budget_total/request_usage/token_usage
无阶段或授权来源字段。本脚本在同一 head 上给出可执行的分账方案并把每一环验证出来:
S0 复现(单账本误配) —— A/B/C 共库时 55 总量挡住 B/C,表格确无阶段/来源字段;
S1 团队测试实例(A) —— 独立库 + total_requests=55/total_tokens=500000 + 当日
类别日顶:按 A 表逐步消费到 55,第 56 次任何类别被拒;
S2 封账 —— 重启不归零;SQLite backup API 一致性快照归档后,归档件
仍是 55/55 且继续拒绝(封账跨重启、跨归档成立);测试档案
只留在团队库;
S3 全新用户实例(B/C) —— 独立新库、不配置一次性总量:A 耗尽后 B(mem0_extract)
与 C(dialogue_main)照常放行;日顶叠加(dialogue 60、
semantic 40、daily_tokens)逐项到顶拒绝;拒绝不落账;
S4 误配守门 —— 用户实例若误写 total 55,B/C 再次被挡(这就是为什么发布
包禁止把 A 总量写进用户实例配置,发布卡按 §6 复核);
S5 冻结保全 —— 全程只写临时目录;仓库配置文件前后哈希不变。
真实 cap 全部保持 0:本脚本不读取也不写入任何生产/权威账(39/60、498/500、冻结 2 条、
HEL-835 闸与 hel857 权威账原样不动)。B 部分的 embed_ops/llm_ops 权威账闸(authority
fail-closed、逐行锚点、公式换算)由既有 verify_semantic_recovery_hel885.py /
verify_mem0_real_hel874.py 在固定 SDK 边界覆盖,不在本脚本重复。
用法:.venv311/bin/python tools/verify_budget_split_hel896.py [输出目录]
"""
from __future__ import annotations
import hashlib
import json
import sqlite3
import sys
import tempfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from cybergril.life.model import PersistentBudget # noqa: E402
from cybergril.memory.store import Store # noqa: E402
from cybergril.think.budget import BudgetConfig, BudgetGuard, BudgetStop # noqa: E402
HEAD = "04eff611e7d85e539d43717711172441420ac853"
RESULTS: list[tuple[str, bool, str]] = []
def check(name: str, ok: bool, detail: str = "") -> None:
RESULTS.append((name, ok, detail))
print(f"{'PASS' if ok else 'FAIL'} {name}" + (f" — {detail}" if detail else ""))
def acquire(guard: PersistentBudget, scene: str, tokens: int = 10, requests: int = 1) -> str:
try:
guard.acquire(scene, tokens=tokens, requests=requests)
return "allowed"
except BudgetStop as exc:
return str(exc)
def snapshot_via_backup_api(src: Path, dst: Path) -> None:
"""一致性快照:SQLite backup API(在线安全;WAL 中已提交行不丢)。"""
with sqlite3.connect(str(src)) as source, sqlite3.connect(str(dst)) as target:
source.backup(target)
def main() -> int:
out_dir = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path(__file__).resolve().parents[1] / "docs/rework/hel896/budget-split"
out_dir.mkdir(parents=True, exist_ok=True)
repo = Path(__file__).resolve().parents[1]
watched = [repo / "config/models.yaml", repo / "config/semantic.example.yaml"]
before_hashes = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in watched}
report: dict = {"head": HEAD, "no_provider_calls": True, "temp_only": True, "scenes": {}}
with tempfile.TemporaryDirectory(prefix="hel896-budget-split-") as tmp:
tmp = Path(tmp)
# ── S0:复现单账本误配(总管 review_hel895_budget.py 的同形复现) ──
s0 = Store(tmp / "single-ledger.db")
guard0 = PersistentBudget(s0, BudgetGuard(BudgetConfig(
daily_tokens=2_000_000,
daily_requests={"dialogue": 60, "life": 8, "background": 40, "semantic": 40},
total_requests=55, total_tokens=500_000)))
for _ in range(55):
guard0.acquire("dialogue_main", tokens=1)
b_blocked = acquire(guard0, "mem0_extract")
c_blocked = acquire(guard0, "dialogue_main")
columns = {t: [r["name"] for r in s0.execute(f"PRAGMA table_info({t})").fetchall()]
for t in ("budget_total", "request_usage", "token_usage")}
check("S0_single_ledger_repro_A55_blocks_B_and_C",
b_blocked.startswith("budget_total_requests_exhausted") and c_blocked.startswith("budget_total_requests_exhausted"),
f"B={b_blocked} C={c_blocked}")
check("S0_tables_have_no_stage_or_source_columns",
all("stage" not in c and "source" not in c and "auth" not in c for c in columns.values()),
json.dumps(columns, ensure_ascii=False))
report["scenes"]["S0_single_ledger"] = {"team_test_used": guard0.total_requests_used(),
"B_after_A": b_blocked, "C_after_A": c_blocked,
"table_columns": columns}
s0.close()
# ── S1:团队测试实例(A)——独立库,一次性总量 + 当日类别日顶,按 A 表逐步消费 ──
team = Store(tmp / "team-8360.db")
team_cfg = BudgetConfig(daily_tokens=2_000_000,
daily_requests={"dialogue": 60, "life": 8, "background": 40, "semantic": 40},
total_requests=55, total_tokens=500_000)
team_guard = PersistentBudget(team, BudgetGuard(team_cfg))
# A 表逐步(budget-plan-hel874 §1):创建/诠释/习惯 4(background)、对话 16(dialogue)、
# 生活 8(life)、判定/成长/gist 20(background)、主动措辞 3(dialogue),再按表耗到硬顶 55。
plan = [("fact_extract", 4), ("dialogue_main", 16), ("life_event", 8), ("memory_gist", 20), ("proactive_message", 3)]
steps_ok = True
for scene, n in plan:
for _ in range(n):
steps_ok &= acquire(team_guard, scene) == "allowed"
while team_guard.total_requests_used() < 55:
steps_ok &= acquire(team_guard, "dialogue_main") == "allowed"
s1_reject = acquire(team_guard, "dialogue_main")
# life 的当日 8 次在 A 表里已用满:拒绝来自更紧的类别日顶(叠加取最紧,同为硬闸);
# semantic 当日还有余量,第 56 次由一次性总量拒绝。
s1_reject_life = acquire(team_guard, "life_event")
s1_reject_semantic = acquire(team_guard, "mem0_extract")
check("S1_team_instance_A_consumes_55_then_hard_stops",
steps_ok and team_guard.total_requests_used() == 55
and s1_reject.startswith("budget_total_requests_exhausted")
and s1_reject_life.startswith("budget_requests_exhausted:life")
and s1_reject_semantic.startswith("budget_total_requests_exhausted"),
f"used={team_guard.total_requests_used()} dialogue={s1_reject} life={s1_reject_life} semantic={s1_reject_semantic}")
team_tokens_used = team_guard.total_tokens_used()
report["scenes"]["S1_team_instance"] = {"used": team_guard.total_requests_used(),
"tokens_used": team_tokens_used,
"step_plan": plan,
"after_55": {"dialogue": s1_reject, "life": s1_reject_life,
"semantic": s1_reject_semantic}}
# ── S2:封账——重启不归零;backup API 归档件同样封死;测试档案只在团队库 ──
team.close()
team_reopened = Store(tmp / "team-8360.db", init=False)
team_guard2 = PersistentBudget(team_reopened, BudgetGuard(team_cfg))
s2_after_restart = acquire(team_guard2, "dialogue_main")
archive = tmp / "team-8360-sealed.db"
snapshot_via_backup_api(tmp / "team-8360.db", archive)
sealed = Store(archive, init=False)
sealed_guard = PersistentBudget(sealed, BudgetGuard(team_cfg))
s2_archive = acquire(sealed_guard, "mem0_extract")
check("S2_seal_survives_restart_and_archive",
team_guard2.total_requests_used() == 55 and s2_after_restart.startswith("budget_total_requests_exhausted")
and sealed_guard.total_requests_used() == 55 and s2_archive.startswith("budget_total_requests_exhausted"),
f"restart={s2_after_restart} archive={s2_archive} used={sealed_guard.total_requests_used()}")
report["scenes"]["S2_seal"] = {"after_restart": s2_after_restart, "archive_after_backup_api": s2_archive}
team_reopened.close()
sealed.close()
# ── S3:全新用户实例(B/C)——独立新库、无一次性总量:A 耗尽后 B/C 照常放行,日顶叠加 ──
user = Store(tmp / "user-fresh.db")
user_cfg = BudgetConfig(daily_tokens=2_000_000,
daily_requests={"dialogue": 60, "life": 8, "background": 40, "semantic": 40},
total_requests=None, total_tokens=None)
user_guard = PersistentBudget(user, BudgetGuard(user_cfg))
b_start = acquire(user_guard, "mem0_extract")
c_start = acquire(user_guard, "dialogue_main")
# 无一次性总量 = 无总量闸:budget_total 行只作记录(2 次消费照实累计),永不禁停。
check("S3_user_instance_B_and_C_unblocked_after_A",
b_start == "allowed" and c_start == "allowed" and user_guard.config.total_requests is None
and user_guard.total_requests_used() == 2,
f"B={b_start} C={c_start} cumulative_counter={user_guard.total_requests_used()} (record-only, no gate)")
# C 日顶叠加:dialogue 60 到顶拒绝;semantic 40 到顶拒绝;日 token 池到顶拒绝;拒绝不落账。
for _ in range(59):
acquire(user_guard, "dialogue_main")
c_daily = acquire(user_guard, "dialogue_main")
for _ in range(39):
acquire(user_guard, "mem0_extract")
b_daily = acquire(user_guard, "mem0_extract")
dialogue_used = user_guard.requests_used("dialogue")
semantic_used = user_guard.requests_used("semantic")
token_cap = acquire(user_guard, "life_event", tokens=2_000_000_000)
check("S3_daily_caps_stack_on_user_instance",
c_daily.startswith("budget_requests_exhausted:dialogue") and b_daily.startswith("budget_requests_exhausted:semantic")
and dialogue_used == 60 and semantic_used == 40 and token_cap.startswith("budget_tokens_exhausted"),
f"dialogue={c_daily} semantic={b_daily} tokens={token_cap} used d={dialogue_used} s={semantic_used}")
report["scenes"]["S3_user_instance"] = {"B_after_A": b_start, "C_after_A": c_start,
"dialogue_daily_reject": c_daily, "semantic_daily_reject": b_daily,
"token_pool_reject": token_cap,
"budget_total_rows": user_guard.total_requests_used()}
user.close()
# ── S4:误配守门——用户实例若误写 total 55,B/C 再次被挡(发布配置复核点) ──
wrong = Store(tmp / "user-wrong-total.db")
wrong_guard = PersistentBudget(wrong, BudgetGuard(BudgetConfig(
daily_tokens=2_000_000, daily_requests={"dialogue": 60, "life": 8, "background": 40, "semantic": 40},
total_requests=55, total_tokens=500_000)))
for _ in range(55):
wrong_guard.acquire("dialogue_main", tokens=1)
s4_b = acquire(wrong_guard, "mem0_extract")
s4_c = acquire(wrong_guard, "dialogue_main")
check("S4_misconfigured_total_on_user_instance_blocks_B_C",
s4_b.startswith("budget_total_requests_exhausted") and s4_c.startswith("budget_total_requests_exhausted"),
f"B={s4_b} C={s4_c}")
report["scenes"]["S4_misconfig"] = {"B": s4_b, "C": s4_c}
wrong.close()
# ── S5:冻结保全——仓库配置与权威事实零改动 ──
after_hashes = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in watched}
check("S5_repo_configs_unchanged_no_authoritative_writes", before_hashes == after_hashes,
json.dumps(before_hashes, ensure_ascii=False))
report["scenes"]["S5"] = {"config_sha256_before": before_hashes, "config_sha256_after": after_hashes}
ok = all(r[1] for r in RESULTS)
report["checks"] = [{"name": n, "ok": o, "detail": d} for n, o, d in RESULTS]
report["ok"] = ok
(out_dir / "budget-split.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps({"ok": ok, "checks": len(RESULTS)}, ensure_ascii=False))
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())
+136 -34
View File
@@ -1,7 +1,20 @@
// HEL-895 real-server browser check: drives headless Chromium over CDP against the REAL product API
// (python -m cybergril.api serving web/dist), not the mock. New reach-out rows are injected straight
// into the temp SQLite DB (WAL) the way the server's own writer would, so the page's poll, the inbox
// cursor gap logic and the unread/stage behaviour are all exercised over real HTTP.
// HEL-895 real-server browser check, HEL-896 revision: drives headless Chromium over CDP against the REAL
// product API (python -m cybergril.api serving web/dist), not the mock. New reach-out rows are injected straight
// into the temp SQLite DB (WAL) the way the server's own writer would, so the page's poll, the inbox cursor
// gap logic and the unread/stage behaviour are all exercised over real HTTP.
//
// HEL-896 fixes the false positives the manager's independent review proved (docs/rework/hel896/):
// - B3 clicked "the first button containing 回" (the 日子与回忆 nav button) and passed on `stage || any
// visible button`. Now it navigates to 日子, waits for the VISIBLE reach card (aside.reach.show) carrying
// exactly this reach-out's words, clicks THAT card's 回她 button, and strictly asserts the conversation
// stage text (.conversation .sr) plus the unread mark clearing. No fallback.
// - B4/B5 dedup evidence is by server message id (data-mid in the record panel), not "1–2 full-page text
// matches"; B5 asserts the restart epoch really is +1 (via /api/health) before re-checking the record.
// - B6 counts the page's own Permissions API / getUserMedia CALLS (instrumented before load), not just states.
// - The service runs with a temp loopback-only models config (CYBERGRIL_MODELS, all caps 0) and a
// sitecustomize socket guard that REJECTS and counts any non-loopback connect, so "zero non-loopback
// outbound" is enforced and evidenced (netguard.json), not assumed. The default /api/health provider probe
// therefore hits a closed loopback port and reports llm.ok=false — that is the isolation working.
// Usage: node tests/hel895_real.mjs <chrome> <venvPython> <repoRoot> <outDir> [port]
import { spawn } from 'node:child_process';
import { mkdirSync, writeFileSync } from 'node:fs';
@@ -11,7 +24,7 @@ import { DatabaseSync } from 'node:sqlite';
const [chrome, venv, root, outArg, portArg] = process.argv.slice(2);
const port = Number(portArg || 8398), base = `http://127.0.0.1:${port}`, cdpPort = 9498;
const out = join(root, outArg || 'docs/rework/hel895');
const out = join(root, outArg || 'docs/rework/hel896/browser');
mkdirSync(out, { recursive: true });
const work = join(tmpdir(), `hel895-real-${Date.now()}`);
mkdirSync(work, { recursive: true });
@@ -20,7 +33,48 @@ const results = [];
const check = (name, ok, detail = '') => { results.push(ok); console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`); };
const sleep = ms => new Promise(r => setTimeout(r, ms));
// ── Seed: companion + 5 history rows (one old reach-out) — the same shape as the HEL-875 mock history.
// ── HEL-896 isolation: loopback-only provider config (caps all 0) + socket guard that rejects non-loopback. ──
const loopbackBase = 'http://127.0.0.1:9/v1'; // nothing listens there: provider probes fail fast on loopback
writeFileSync(join(work, 'models-loopback.yaml'), [
'# HEL-896 browser-check isolation config: loopback-only provider, every budget cap 0 (fail-closed).',
'tiers:',
' high: {provider: openai_compat, model: loopback-closed, base_url: "' + loopbackBase + '", api_key_env: null, timeout: 2.0, temperature: 0.1}',
' standard: {provider: openai_compat, model: loopback-closed, base_url: "' + loopbackBase + '", api_key_env: null, timeout: 2.0, temperature: 0.1}',
' low: {provider: openai_compat, model: loopback-closed, base_url: "' + loopbackBase + '", api_key_env: null, timeout: 2.0, temperature: 0.1}',
'budget:',
' daily_tokens: 2000000',
' daily_requests: {dialogue: 0, life: 0, background: 0, semantic: 0}',
'embedding: {provider: openai_compat, model: loopback-closed, base_url: "' + loopbackBase + '", api_key_env: null, dim: 1024}',
'asr: {provider: scripted, model: none, language: zh}',
'tts: {provider: silence, model: none}',
'vad: {provider: energy, model: none}',
'turn: {provider: heuristic, silence_min_ms: 150, silence_hard_end_ms: 800}',
'paralinguistic: {provider: scripted, model: none, enabled: false}',
'avatar: {route: none, renderer: none, lipsync: envelope}',
''].join('\n'));
const guard = join(work, 'guard');
mkdirSync(guard, { recursive: true });
// The guard writes its report EAGERLY (import time + every event, tmp+rename): a uvicorn SIGTERM exit can die
// by signal without running atexit, and a missing report must never be read as "zero attempts".
writeFileSync(join(guard, 'sitecustomize.py'), [
'import json, os, socket, tempfile',
"LOOP = {'127.0.0.1', '::1', 'localhost'}",
"state = {'guard_loaded': True, 'nonloopback_connect_attempts': 0, 'nonloopback_dns_lookups': 0, 'rejected': []}",
'def _host(a):\n try:\n return a[0]\n except Exception:\n return str(a)',
'def _dump():\n try:\n d = os.path.dirname(os.environ["NETGUARD_OUT"])\n fd, tmp = tempfile.mkstemp(dir=d)\n with os.fdopen(fd, "w") as f:\n json.dump(state, f)\n os.replace(tmp, os.environ["NETGUARD_OUT"])\n except Exception:\n pass',
'_connect = socket.socket.connect',
'def connect(self, address):\n if _host(address) not in LOOP:\n state["nonloopback_connect_attempts"] += 1\n state["rejected"].append(str(address))\n _dump()\n raise ConnectionRefusedError("non-loopback blocked: " + str(address))\n return _connect(self, address)',
'socket.socket.connect = connect',
'_create = socket.create_connection',
'def create_connection(address, *a, **k):\n if _host(address) not in LOOP:\n state["nonloopback_connect_attempts"] += 1\n state["rejected"].append(str(address))\n _dump()\n raise ConnectionRefusedError("non-loopback blocked: " + str(address))\n return _create(address, *a, **k)',
'socket.create_connection = create_connection',
'_gai = socket.getaddrinfo',
'def getaddrinfo(host, *a, **k):\n if host not in LOOP:\n state["nonloopback_dns_lookups"] += 1\n _dump()\n return _gai(host, *a, **k)',
'socket.getaddrinfo = getaddrinfo',
'_dump()',
''].join('\n'));
// ── Seed: companion + 5 history rows (one old reach-out) — the same shape as the HEL-875 mock history. ──
const T0 = Date.UTC(2026, 9, 25, 12);
const identity = { id: '1', name: '栖', birth: '2000-03-14', hour: '子时', relation: '朋友', nickname: '', boundary: '', color: { pillars: { year: '庚辰', month: '己卯', day: '丙午', hour: '戊子' }, traits: 'HEL-895 真实服务浏览器检查合成档案' } };
let seeded = 0;
@@ -28,7 +82,7 @@ const seedRow = (role, text, turn, proactive = 0) => { seeded++; db.prepare('INS
let db = new DatabaseSync(dbPath);
db.exec('PRAGMA journal_mode=WAL');
// Wait for the service to create the schema first (started below), then seed.
const env = { ...process.env, CYBERGRIL_DB: dbPath, PYTHONPATH: join(root, 'src'), CYBERGRIL_R2_ENABLED: '0', CYBERGRIL_SEMANTIC: 'off', CYBERGRIL_PORT: String(port), CYBERGRIL_WEB: join(root, 'web', 'dist') };
const env = { ...process.env, CYBERGRIL_DB: dbPath, PYTHONPATH: guard + ':' + join(root, 'src'), NETGUARD_OUT: join(work, 'netguard.json'), CYBERGRIL_MODELS: join(work, 'models-loopback.yaml'), CYBERGRIL_R2_ENABLED: '0', CYBERGRIL_SEMANTIC: 'off', CYBERGRIL_PORT: String(port), CYBERGRIL_WEB: join(root, 'web', 'dist') };
let svc = spawn(venv, ['-m', 'cybergril.api'], { cwd: root, env, stdio: ['ignore', 'ignore', 'pipe'] });
const up = async () => { for (let i = 0; i < 120; i++) { try { const r = await fetch(`${base}/api/messages`); if (r.status === 404) return true; } catch { await sleep(500); } } return false; };
if (!await up()) { console.error('service did not come up'); process.exit(1); }
@@ -39,9 +93,10 @@ seedRow('companion', '会想把窗开一条缝,听雨落在铁皮檐上。', '
seedRow('companion', '昨晚的月亮很亮,想起你说过喜欢夜里散步。', 'reach-old', 1);
seedRow('user', '今天加班到九点,好累。', 't-2');
seedRow('companion', '那先别急着做别的,喝口热水。', 't-2');
const restart = async () => { svc.kill('SIGTERM'); await new Promise(r => svc.on('exit', r)); svc = spawn(venv, ['-m', 'cybergril.api'], { cwd: root, env, stdio: ['ignore', 'ignore', 'pipe'] }); for (let i = 0; i < 120; i++) { try { await fetch(`${base}/api/health`); return; } catch { await sleep(500); } } };
const rowIdOf = text => db.prepare('SELECT id FROM chat_message WHERE text=? ORDER BY id DESC LIMIT 1').get(text)?.id;
const restart = async () => { svc.kill('SIGTERM'); await new Promise(r => svc.on('exit', r)); svc = spawn(venv, ['-m', 'cybergril.api'], { cwd: root, env, stdio: ['ignore', 'ignore', 'pipe'] }); for (let i = 0; i < 120; i++) { try { const r = await fetch(`${base}/api/health`); if (r.ok) return await r.json(); } catch { await sleep(500); } } return null; };
// ── Chromium over CDP (same driver style as tests/hel875_shots.mjs).
// ── Chromium over CDP (same driver style as tests/hel875_shots.mjs). ──
const profile = join(tmpdir(), `hel895-chrome-${Date.now()}`);
const browser = spawn(chrome, ['--headless=new', `--remote-debugging-port=${cdpPort}`, `--user-data-dir=${profile}`, '--no-first-run', '--hide-scrollbars', '--force-color-profile=srgb', 'about:blank'], { stdio: 'ignore' });
let targets;
@@ -55,6 +110,16 @@ const js = async expr => { const r = await cdp('Runtime.evaluate', { expression:
const shot = async name => { const { data } = await cdp('Page.captureScreenshot', { format: 'png' }); writeFileSync(join(out, `real-${name}.png`), Buffer.from(data, 'base64')); };
const read = async () => { await js('document.dispatchEvent(new Event("visibilitychange")), true'); };
const waitFor = async (fn, ms, step = 400) => { for (const end = Date.now() + ms; Date.now() < end;) { const v = await fn(); if (v) return v; await sleep(step); } return null; };
// HEL-896: count the page's OWN permission API calls from the very first document load.
await cdp('Page.enable');
await cdp('Page.addScriptToEvaluateOnNewDocument', { source: `(() => { let q = 0, g = 0; const qp = navigator.permissions.query.bind(navigator.permissions);
navigator.permissions.query = d => { q++; return qp(d); };
const gm = navigator.mediaDevices && navigator.mediaDevices.getUserMedia && navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
if (gm) navigator.mediaDevices.getUserMedia = d => { g++; return gm(d); };
window.__permCounts = () => ({ query: q, getUserMedia: g }); })()` });
const talkUnread = () => js(`(() => { const b = [...document.querySelectorAll('.nav-item')].find(e => e.textContent.trim() === '交谈');
return b ? { unread: b.classList.contains('unread'), label: b.getAttribute('aria-label') || '' } : null })()`);
const navTo = async label => { await js(`[...document.querySelectorAll('.nav-item')].find(e => e.textContent.trim() === ${JSON.stringify(label)})?.click(), true`); await sleep(1200); };
try {
// B1 — the built web boots against the real service.
@@ -63,47 +128,84 @@ try {
check('B1_real_service_boot', ready, await js(`document.body.innerText.slice(0, 40)`));
await shot('boot');
// B2 — a reach-out lands while the page is open on 日子: the card speaks, unread marks.
// B2 — a reach-out lands while the page is open on 此刻: the letter carries her exact words, the talk nav marks unread.
seedRow('companion', '窗外的雨停了,想跟你说一声。', `reach-live-${Date.now()}`, 1);
await read();
const card = await waitFor(() => js(`!![...document.querySelectorAll('*')].find(e => e.children.length === 0 && e.textContent.includes('窗外的雨停了'))`), 30000);
check('B2_reach_card_over_real_http', !!card);
const letter = await waitFor(async () => { await read(); return js(`(() => { const b = document.querySelector('button.letter:not(.read)');
return b && b.querySelector('p')?.textContent.includes('窗外的雨停了') ? b.querySelector('p').textContent : null })()`); }, 30000, 1500);
const unreadBefore = await waitFor(async () => { const u = await talkUnread(); return u && u.unread ? u : null; }, 8000);
check('B2_reach_card_over_real_http', !!letter && !!unreadBefore, `letter=${!!letter} unread=${unreadBefore?.label || 'none'}`);
await shot('card');
// B3 — replying opens the conversation; the new message plays on the stage.
const unreadHit = await waitFor(() => js(`[...document.querySelectorAll('button, .unread, [class*=unread]')].some(e => e.offsetParent !== null)`), 5000);
if (await js(`[...document.querySelectorAll('button')].some(b => b.textContent.includes('回'))`)) { await js(`[...document.querySelectorAll('button')].find(b => b.textContent.includes('回'))?.click(), true`); await sleep(1500); }
// B3 — the reach card speaks over 日子 and its OWN 回她 button opens the conversation: the reach-out plays on
// the stage and the unread mark clears. Precise: no "any button with 回", no `|| unread` fallback.
await navTo('日子');
const card = await waitFor(() => js(`(() => { const a = document.querySelector('aside.reach.show');
return a && a.getAttribute('aria-hidden') === 'false' && a.getBoundingClientRect().width > 0 && a.querySelector('p')?.textContent.includes('窗外的雨停了') ? a.querySelector('p').textContent : null })()`), 8000);
check('B3a_visible_reach_card', !!card, card ? '' : 'aside.reach.show not visible');
const clicked = await js(`(() => { const a = document.querySelector('aside.reach.show'); if (!a) return false;
const b = [...a.querySelectorAll('.acts button')].find(e => e.textContent.trim() === '回她'); if (!b) return false; b.click(); return true })()`);
await sleep(1500);
const stage = await waitFor(() => js(`(document.querySelector('.conversation .sr')?.textContent || '').includes('窗外的雨停了')`), 8000);
check('B3_stage_plays_reachout', !!stage || !!unreadHit, `stage=${!!stage}`);
const unreadAfter = await talkUnread();
check('B3b_reply_opens_stage_plays_reachout', !!card && !!clicked && !!stage && !unreadAfter?.unread,
`card=${!!card} clicked=${!!clicked} stage=${!!stage} unreadAfter=${unreadAfter?.unread}`);
await shot('stage');
// B4 — long absence: 260 bulk rows land ABOVE one new reach-out (gap regression, HEL-880):
// the bounded first read (5×50) must not skip her; the next read closes the gap and hands her over.
// B4 — long absence: 260 bulk rows land ABOVE one new reach-out (gap regression, HEL-880): the bounded first
// read (5×50) must not skip her; the next read closes the gap and hands her over — proven by server message id.
seedRow('companion', '刚看到一只猫从窗台跳下去。', `reach-gap-${Date.now()}`, 1);
for (let i = 0; i < 260; i++) seedRow(i % 2 ? 'companion' : 'user', `别处的第${i + 1}句`, `bulk-${Date.now()}-${i}`);
await read();
const gapDone = await waitFor(async () => {
await read();
return js(`[...document.querySelectorAll('*')].some(e => e.children.length === 0 && e.textContent.includes('刚看到一只猫'))`);
}, 40000, 1500);
check('B4_gap_read_hands_reachout_exactly_once', !!gapDone);
const gapId = rowIdOf('刚看到一只猫从窗台跳下去。'), liveId = rowIdOf('窗外的雨停了,想跟你说一声。');
const badge = await waitFor(async () => { await read(); const u = await talkUnread(); return u && u.unread ? u : null; }, 40000, 1500);
check('B4a_gap_read_hands_reachout', !!badge, `gapId=${gapId} ${badge?.label || 'no badge'}`);
await shot('gap');
const dup = await js(`[...document.querySelectorAll('*')].filter(e => e.children.length === 0 && e.textContent.includes('刚看到一只猫')).length`);
check('B4b_no_duplicate_after_gap', dup >= 1 && dup <= 2, `matches=${dup}`);
// Leaving the conversation merges the queued reach-out into the record (the one unread rule); the record's
// data-mid entries are the server's own ids, so dedup is proven per message id, not by counting text matches.
await navTo('日子'); await navTo('交谈');
await js(`document.querySelector('button[aria-label="完整记录"]')?.click(), true`); await sleep(1200);
const recordIds = () => js(`[...document.querySelectorAll('.record [data-mid]')].map(e => e.getAttribute('data-mid'))`);
let ids = await waitFor(async () => { const v = await recordIds(); return v && v.includes(String(gapId)) ? v : null; }, 10000);
const dupCount = ids ? ids.length - new Set(ids).size : -1;
const gapHits = ids ? ids.filter(i => i === String(gapId)).length : 0;
const liveHits = ids ? ids.filter(i => i === String(liveId)).length : 0;
check('B4b_no_duplicate_by_message_id', !!ids && dupCount === 0 && gapHits === 1 && liveHits === 1,
`rows=${ids?.length || 0} dups=${dupCount} gapId×${gapHits} liveId×${liveHits}`);
await shot('record');
await js(`document.querySelector('button[aria-label="关闭记录"]')?.click(), true`); await sleep(600);
// B5 — server restart (epoch+1): the same rows must not be added again.
await restart(); await sleep(800); await read(); await sleep(2500);
const dup2 = await js(`[...document.querySelectorAll('*')].filter(e => e.children.length === 0 && e.textContent.includes('刚看到一只猫')).length`);
check('B5_restart_epoch_no_dup', dup2 >= 1 && dup2 <= 2, `matches=${dup2}`);
// B5 — server restart: the epoch really bumps by exactly one, and the same rows are not added again.
const epochBefore = (await (await fetch(`${base}/api/health`)).json()).epoch;
const healthAfter = await restart(); await sleep(800);
const epochAfter = healthAfter?.epoch;
await read(); await sleep(2500); await read(); await sleep(1500);
await js(`document.querySelector('button[aria-label="完整记录"]')?.click(), true`); await sleep(1200);
ids = await recordIds();
const dup2 = ids ? ids.length - new Set(ids).size : -1;
const gap2 = ids ? ids.filter(i => i === String(gapId)).length : 0;
check('B5_restart_epoch_plus1_no_dup', epochAfter === epochBefore + 1 && dup2 === 0 && gap2 === 1,
`epoch=${epochBefore}->${epochAfter} rows=${ids?.length || 0} dups=${dup2} gapId×${gap2}`);
// B6 — no permission was ever asked for.
// B6 — the page never called the Permissions API or getUserMedia at all (instrumented from first load).
const counts = await js('window.__permCounts()');
const mic = await js(`navigator.permissions.query({name:'microphone'}).then(r => r.state)`);
const cam = await js(`navigator.permissions.query({name:'camera'}).then(r => r.state)`);
check('B6_zero_permission_requests', mic === 'prompt' && cam === 'prompt', `mic=${mic} cam=${cam}`);
check('B6_zero_permission_api_calls', !!counts && counts.query === 0 && counts.getUserMedia === 0,
`counts=${JSON.stringify(counts)} mic=${mic} cam=${cam}`);
} catch (e) {
console.error('driver error:', e.message);
results.push(false);
} finally {
writeFileSync(join(out, 'real-browser.json'), JSON.stringify({ ok: results.every(Boolean), checks: results.length, fail_count: results.filter(v => !v).length }, null, 1));
try { browser.kill(); } catch { /* already gone */ }
// HEL-896: the service must EXIT so the socket guard's atexit report exists; non-loopback budget is then read
// from its own counter file, not assumed.
try { svc.kill('SIGTERM'); } catch { /* already gone */ }
await new Promise(r => { if (svc.exitCode !== null || svc.signalCode) return r(); svc.on('exit', r); setTimeout(r, 8000); });
let netguard = null;
try { netguard = JSON.parse((await import('node:fs')).readFileSync(join(work, 'netguard.json'), 'utf8')); } catch { /* missing report = honest fail below */ }
check('B7_zero_nonloopback_outbound', !!netguard && netguard.nonloopback_connect_attempts === 0,
netguard ? JSON.stringify(netguard) : 'netguard.json missing');
writeFileSync(join(out, 'real-browser.json'), JSON.stringify({ ok: results.every(Boolean), checks: results.length, fail_count: results.filter(v => !v).length, netguard: netguard || 'missing', service_epoch_isolation: 'CYBERGRIL_MODELS loopback, caps 0, socket guard rejecting non-loopback' }, null, 1));
try { browser.kill(); } catch { /* already gone */ }
ws.close();
}
process.exit(results.every(Boolean) ? 0 : 1);