HEL-879 release closure: the four HEL-896 audit findings become enforced behavior - (1) archive consistency: tools/release_snapshot_hel896.py v2 checks the wal_checkpoint(TRUNCATE) return (busy!=0 = hard failure, no deliverable tar, half-built dir removed - 'stopped service first' is a precondition, never a reason to ignore an actual failed result), aborts packaging on any source-vs-snapshot table-count mismatch instead of recording rows_match=False and shipping anyway, and verify_archive now rejects legacy packages that recorded rows_match=False while mapping a real rollback drill (unpack to an isolated dir, open every DB, check rows AND hashes against both recorded source and snapshot counts, emit a source-path rollback_plan - no more tar-internal self-attestation); selftest 9/9 reproduces every old failure shape first (naive WAL copy loses companion, held-reader checkpoint=(1,9,8) leaves the copied image malformed/missing the events row, the manager's rows_match=False legacy package verifying ok) then proves each fix (busy fails with zero artifacts, stale-snapshot mismatch fails with zero artifacts, online mode refuses --file dirs containing SQLite by magic-header scan, stop mode recursively takes embedded SQLite like history_root/mem0_public_history.db through checkpoint+per-table verification, verify maps source paths and passes); (2) early-finish seal: PersistentBudget gains sealed()/seal() writing sealed/sealed_requests/sealed_tokens/sealed_at atomically into budget_total beside the real usage (never faking 55, never zeroing), acquire checks the seal FIRST so every category and every remaining allowance rejects budget_sealed:51/55, the marker survives restart and backup-API archive/restore, reseal is idempotent, and there is deliberately no unseal path (re-test = new approval + new instance per budget-package 6.2.4); tools/seal_budget_hel879.py makes the closure an explicit auditable operator command with a post-seal rejection self-check; verify_budget_split_hel896.py gains S6 (51/55 experience chain completes -> seal keeps usage 51 -> 52nd and cross-category rejected with dialogue daily still 19/60 proving it is the seal not the caps, mistaken restart rejected, archive restore rejected, marker requests=51 intact) now 10/10, HEAD made dynamic instead of hardcoded 04eff61; budget-package-hel895.md 6.2 rewritten to the executable seal order (stop -> seal -> consistency snapshot); (3) single install entry: deploy/hel895-release-candidate.md 2 is now the only copy-paste install (uv --offline, npm --offline, cache-miss = stop and record, network only inside an authorized release window with per-item sha256) and 4 is the only fixed-SDK entry as a five-step chain (verify tarball sha256 9dcef930...dda23 against mem0-stack.lock -> build the wheel from the fixed source with hatchling honestly listed as the missing build backend -> cp311 dependency prep with greenlet/pillow/truststore confirmed cache-reusable on re-run -> local-wheels-only --no-index install where mem0ai must come from the fixed-source wheel, the lock line is version-only -> same-process co-import plus the 92MB fastembed cache placement); deploy/hel874-release.md 2/4 carry superseded banners pointing here, mem0-stack.lock no longer routes installs through the index mem0ai==2.2.1 wheel; (4) exact-commit honesty: audit_sdk_offline_hel896.py derives head from git rev-parse and accepts an output dir, fresh evidence written to docs/rework/hel879/sdk-audit/ (hel896 evidence left untouched) recording closure 55 / missing 41 (28 pure + 13 native) / cache-reusable [greenlet, pillow, truststore] / tarball-matches-lock at c121c42; regression on this head: pytest 299/299, ruff clean (src/tests/tools), release-snapshot selftest 9/9, budget-split 10/10 including the new S6 negatives; zero provider calls, zero downloads, authoritative ledgers/freezes/HEL-835 gate untouched, not deployed, PR#11 untouched

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
zonggong
2026-10-27 03:26:30 +08:00
co-authored by multica-agent
parent c121c42e39
commit 01ff919859
13 changed files with 1682 additions and 142 deletions
+9
View File
@@ -20,6 +20,10 @@ mem0 同进程导入的准确缺项与不下载解决路径、空账初始化、
## 2. 安装命令(发布卡执行)
> **已被取代(HEL-879,2026-10-27)**:本节安装命令缺 `--offline`/`--no-index`
> 强制项。唯一照抄入口改为 `deploy/hel895-release-candidate.md` §2。本文件其余
> 内容作为历史交付证据保留原样。
```bash
# 全新目录,逐条照抄;任何一步失败即停
# 2.1 服务账号与目录(独立可写数据路径,权限边界明确)
@@ -63,6 +67,11 @@ Mem0 提取都会在发送前被硬闸拒绝(`budget_*_exhausted`)——这
## 4. 真实语义激活前置(B 部分批准后;准确缺项与不下载路径)
> **已被取代(HEL-879,2026-10-27)**:本节 4.1/4.2 按索引拉 `mem0ai==2.2.1`
> 同名 wheel,不能保证固定源码身份 2c81af3;固定 SDK 激活的唯一照抄入口改为
> `deploy/hel895-release-candidate.md` §4(固定 tarball 哈希 → 固定源码构建轮 →
> cp311 依赖准备 → 仅本地轮安装 → 同进程 co-import)。以下保留为历史证据。
**当前事实(HEL-882 离线核实,tools/check_release_offline_hel882.py R6):**
固定安装 hel857-venv 是 Python 3.10.12,产品发布 venv 是 3.11.16,两者
site-packages 不能混用;本机 uv/pip 缓存里没有 mem0ai/fastembed/qdrant-client
+111 -41
View File
@@ -1,5 +1,17 @@
# HEL-895 · 统一发布候选(离线核验过,未执行部署)
> **HEL-879 发布工具收口(2026-10-27,隔离候选分支 `hel-879-assembly`)**:按总管
> 对 HEL-896 的独立复核完成四项有限收口——①归档一致性:checkpoint 返回值必查、
> 源/快照行数不一致或 checkpoint busy 一律 CLI 非 0 且不形成可交付包,verify 拒绝
> 记录了 `rows_match=False` 的旧包(§5);②目录内真实 SQLite(history_root 的
> `mem0_public_history.db` 等)停服口径走 checkpoint+逐表核对,在线模式只允许显式
> 单库 backup API、含 SQLite 的目录直接拒绝;③提前封账:`tools/seal_budget_hel879.py`
> 显式封账(真实用量保留、跨重启/归档拒绝,负例 S6);④安装入口唯一化:本文件 §2/§4
> 为唯一照抄入口(全 offline/no-index;mem0ai 只认固定源码轮),`deploy/hel874-release.md`
> §2/§4 已标注被取代。产物清单与精确提交见 `docs/rework/hel879/sdk-audit/` 与
> `docs/rework/hel879/release-artifacts.json`。**不要用旧父提交(04eff61/152479b)
> 的说明冒充本候选 head:以交付评论与 handoff JSON 记录的精确提交为准。**
> **HEL-896 修订(2026-10-27,正常追加 PR#11)**:按总管独立复审补齐——①浏览器
> 「真实服务 7/7」中 B3 原为已证实误报,测试已修为精确交互+严格断言(9 项检查,
> 含权限 API 调用计数与非回环出站守卫,证据 `docs/rework/hel896/browser/`);
@@ -51,13 +63,36 @@ HEL-881 已审核的主动留言前端(`bf234570`)装成一个统一候选
| 预算执行包 | `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. 安装命令(发布卡执行)
## 2. 安装命令(发布卡执行;本节是唯一照抄入口——HEL-879 收口)
与 `deploy/hel874-release.md` §2 完全相同(服务账号 → 全新空目录 clone 本 head →
`uv venv --python 3.11` + `uv pip install -r requirements.lock` → `npm ci &&
npm run build` → 安装 service → `curl /api/health`),此处不重复抄写;唯一差别是
`git checkout <本文件 §0 的精确 head>`。发布前必过检查同样按 `deploy/hel874-release.md`
§6 执行,另加本卡两个新工具(真实 HTTP 分页合同、真实服务浏览器)在目标机重跑。
```bash
# 全新目录,逐条照抄;任何一步失败即停。全部安装强制 offline/no-index:
# 缓存命中即装,缓存缺失即停并留证;联网补齐只发生在获批的发布窗口内,
# 逐项 sha256 留证(seq48–51 勘误教训:不写 offline 的安装发生过网络获取)。
# 2.1 服务账号与目录(独立可写数据路径,权限边界明确)
useradd --system --home-dir /opt/cybergril-hel874 --shell /usr/sbin/nologin cybergril || true
mkdir -p /opt/cybergril-hel874/data # data 必须为空:新账从零初始化
chown -R cybergril:cybergril /opt/cybergril-hel874
# 2.2 代码 checkout:全新空目录 clone(空目录里没有仓库,不能直接 git fetch)
git clone ssh://git@192.168.200.36:222/leefer/CyberGril.git /opt/cybergril-hel874/current
git -C /opt/cybergril-hel874/current checkout <交付评论/handoff JSON 记录的精确head>
# 2.3 依赖与前端(uv/npm 一律 offline;缓存缺失即停)
cd /opt/cybergril-hel874/current
uv venv --python 3.11 .venv311
uv pip install --python .venv311/bin/python --offline -r requirements.lock
(cd web && npm ci --offline && npm run build)
# 2.4 服务单元(单元内已带 PYTHONPATH=<current>/src:requirements.lock 只装依赖,
# 产品代码经 PYTHONPATH 进程内可 import)
cp deploy/cybergril-hel874-8360.service.example /etc/systemd/system/cybergril-hel874.service
systemctl daemon-reload && systemctl enable --now cybergril-hel874
curl -s http://127.0.0.1:8360/api/health | python3 -m json.tool
```
发布前必过检查按 §6 与 `deploy/hel874-release.md` §6 的清单执行(该节仍有效),
另加本卡工具(真实 HTTP 分页合同、真实服务浏览器)在目标机重跑。
## 3. 健康能力矩阵(/api/health 预期)
@@ -68,38 +103,60 @@ weather/materials/backup=false。**主动留言前端已并入本候选:生活
真实 reach-out 后,打开的页面每 20 秒读一次新行、回来立即读,未读有标记;该行为
已在本 head 用真实服务验证(§0)。**
## 4. 固定 SDK(3.11)准确缺项与不下载路径
## 4. 固定 SDK(3.11)激活——唯一照抄入口(HEL-879 收口)
**HEL-896 只读审计修订(2026-10-27,`tools/audit_sdk_offline_hel896.py`,证据
`docs/rework/hel896/sdk-audit/`)**:此前「只缺 mem0ai/qdrant-client/fastembed 三个
cp311 wheel 及纯 Python 传递依赖」的概括**不准确**,勘误如下:
**本节取代 `deploy/hel874-release.md` §4(历史文件,已标注被取代,不得再照抄)。**
旧 4.1/4.2 按 `deploy/mem0-stack.lock` 的 `mem0ai==2.2.1` 向索引拉同名 wheel——
**索引同名 wheel 不能证明固定源码身份 2c81af3**,这条路已废除。入口只有一条:
- 三个顶层包固定版本(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`。**
**固定 tarball 哈希 → 固定源码构建轮 → 目标 cp311 依赖准备 → 仅本地轮安装 → 同进程 co-import。**
- **第 0 步 · 产物清单核对**:`docs/rework/hel879/sdk-audit/sdk-audit.json`(本候选
精确提交上重跑)与 `docs/rework/hel879/release-artifacts.json` 记录闭包 55 项、
缺 41 项、缓存可复用 3 项与逐项哈希;发布卡先核对清单与目标提交一致。
事实口径(HEL-896 审计核定,HEL-879 重跑仍成立):三个顶层固定版本
(mem0ai 2.2.1 / qdrant-client 1.20.0 / fastembed 0.9.1)的 wheel 均为
`py3-none-any` 纯 Python;闭包按 3.11 Linux markers+extras 共 **55 项,产品
3.11 venv 缺 41 项**(28 纯 Python + 13 原生:grpcio/numpy/onnxruntime/
tokenizers/sqlalchemy/protobuf 等),这是闭包清单不是已完成的 cp311 下载结果。
seq48–51 pip 事件勘误保留:不写 offline 的安装发生过网络获取(uv 缓存取证),
故本入口全程 `--offline`/`--no-index`。
- **第 1 步 · 固定 tarball 哈希核对**(零下载):
```bash
sha256sum <hel857-data>/mem0-2c81af3.tar.gz
# 必须等于 deploy/mem0-stack.lock 记录的
# 9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23
```
不一致即停;不得改用索引 `mem0ai==2.2.1` wheel 冒充固定源(audit 已证明两者
身份不可互证)。
- **第 2 步 · 固定源码构建轮**:`python3 -m pip wheel --no-deps --no-index
--no-build-isolation -w wheels/ mem0-2c81af3.tar.gz`。**前置缺口如实保留**:
build backend `hatchling` 当前不在任何本机缓存(audit 实测),未获批不联网补齐;
hatchling 落位后本命令完全离线,构建出的 wheel 逐项 sha256 留证。
- **第 3 步 · 目标 cp311 依赖准备**:41 项缺项中**缓存今天即可复用 3 项**
(greenlet 3.5.6 cp311、pillow 12.3.0 cp311、truststore 0.10.5 py3-none-any,
uv wheels-v6/v7 缓存实测,HEL-879 重跑核实仍命中);其余 37 项 + hatchling
**由总管获批后发布窗口拉取**(`python3 -m pip download --only-binary :all:
--implementation cp --python-version 3.11 --platform manylinux2014_x86_64
--abi cp311 -r <缺项清单> -d wheels/`),逐项 sha256 留证;本卡零下载、零联网。
某固定版本若无 cp311 wheel,按 lock 注释如实上报缺项,不换版本号、不混 sdist。
- **第 4 步 · 仅本地轮安装**(--no-index 全程,目录里只有上两步产物):
```bash
uv pip install --python .venv311/bin/python --no-index \
--find-links <本地wheels目录> mem0ai==2.2.1 qdrant-client==1.20.0 fastembed==0.9.1 <41缺项>
```
`deploy/mem0-stack.lock` 的 mem0ai 行只作版本记录;安装来源必须是第 2 步的
固定源码轮(同版本号不同来源不互认)。
- **第 5 步 · 同进程 co-import 与模型落位**:
```bash
.venv311/bin/python -c "import cybergril.api, mem0, qdrant_client, fastembed; print('co-import ok')"
```
fastembed 模型缓存(hel857-data/fastembed-cache,Qdrant/bge-small-zh-v1.5 +
bm25 约 92MB,实测存在)拷入实例数据目录,真实 mem0 模式 cache_dir 指向它。
产品 venv 目前 `import mem0/qdrant_client/fastembed` 均缺、产品代码导入不受影响;
在 wheel 落位且 co-import 通过前,不得以 stub/FTS 冒充真实 Mem0 体验就绪。
真缺的下载(37 项 + hatchling)仍待主卡授权,本卡不为补命令联网。
## 5. 停用与保留数据回退
@@ -108,26 +165,39 @@ cp311 wheel 及纯 Python 传递依赖」的概括**不准确**,勘误如下
`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 逐库一致性快照**。照抄命令:
**HEL-896 归档/回退修订 + HEL-879 收口(2026-10-27)**:在线库只复制主 `.db` 会丢
WAL 里已提交的资料(总管复现件连 `companion` 表都不在)。发布归档一律二选一:
**停服务 + `PRAGMA wal_checkpoint(TRUNCATE)` 后整目录归档**(推荐),或**在线用
SQLite backup API 对显式单库做一致性快照**。工具已按收口语义强制执行(v2):
- checkpoint 返回 busy≠0 或源/快照行数不一致 → CLI 非 0、不产生可交付归档
(「先停服」是前提,不是忽略失败结果的理由);
- 目录内真实 SQLite(history_root 的 `mem0_public_history.db`、向量目录内库等,
按文件头魔数递归识别)在停服口径一并 checkpoint+逐表核对;
- 在线模式只接受显式 `--db`(backup API);传入含 SQLite 的 `--file` 目录直接拒绝;
- `verify` 拒绝记录了 `rows_match=False` 的旧包,回退演练按源路径映射解包到隔离
新目录、实际打开各库核对行数与哈希,输出 rollback_plan。
照抄命令:
```bash
# 覆盖范围:产品库(CYBERGRIL_DB)、语义向量目录 vector_root、推导史 history_root、
# 权威嵌入账 ledger_path(按部署实例 config/semantic.yaml 实际路径逐项代入)
# 停服口径(推荐;目录内 SQLite 一并一致性处理):
.venv311/bin/python tools/release_snapshot_hel896.py snapshot \
--name <release-或阶段名> --db <产品库> [--db <权威账>] \
--file <vector_root> --file <history_root> --out /opt/<实例>/archives/
# 在线兜底口径:仅显式单库(backup API),不得传含 SQLite 的目录:
.venv311/bin/python tools/release_snapshot_hel896.py snapshot --name <名> \
--db <产品库> --out /opt/<实例>/archives/ --online
.venv311/bin/python tools/release_snapshot_hel896.py verify --archive <产物>.tar.gz
```
产物含逐文件 SHA256 清单、**代码精确提交**(快照时刻 `git rev-parse HEAD`)、
逐表行数核对与回退演练结果;不删档,旧归档永久保留。回退=停服务 → 解包快照到
新数据目录 → 指回上一 release/上一提交 → 起 service → `verify` 通过后交付。
本节是**发布归档/回退要求**,不冒充 R6(CG-009 每日自动备份/手动完整导出)完成。
新数据目录(按 rollback_plan 的源路径映射)→ 指回上一 release/上一提交 → 起
service → `verify` 通过后交付。本节是**发布归档/回退要求**,不冒充 R6(CG-009
每日自动备份/手动完整导出)完成。
## 6. 本候选不做的事
+6 -4
View File
@@ -1,11 +1,13 @@
# CyberGril 语义记忆层固定栈清单(HEL-882;B 部分批准后由发布卡拉取)
# 固定源:mem0ai 2.2.1(源码提交 2c81af3bbe27d9bccb09c772d583df9e9be72ee5,
# tarball SHA256 9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23)。
# 用于 deploy/hel874-release.md §4:在具备内网源权限的环境按 3.11 平台拉全
# wheel 后离线并入发布 venv;本卡不执行下载。版本来自 hel857-venv 实测安装
# HEL-879 唯一安装入口:deploy/hel895-release-candidate.md §4(固定 tarball 哈希 →
# 固定源码构建轮 → cp311 依赖准备 → 仅本地轮安装 → 同进程 co-import)。
# 本文件的 mem0ai==2.2.1 行只作版本记录——不得按此行向索引拉同名 wheel 冒充固定源
# (旧 deploy/hel874-release.md §4 已被取代)。若某包无 3.11 wheel,发布卡须如实
# 上报缺项,不得换版本号冒充固定安装。版本来自 hel857-venv 实测安装
#(Python 3.10.12 侧 mem0ai 2.2.1 / qdrant-client 1.20.0 / fastembed 0.9.1),
# 3.11 侧需同版本 wheel;若某包无 3.11 wheel,发布卡须如实上报缺项,不得
# 换版本号冒充固定安装。
# 3.11 侧需同版本 wheel。
mem0ai==2.2.1
qdrant-client==1.20.0
fastembed==0.9.1
+9 -4
View File
@@ -113,10 +113,15 @@ HEL-857 的 60 次实验授权);B/C 需白栖知明确批准。
`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 验证),
2. **A 结束即封账**,两种到达方式都有可执行动作(HEL-879 收口):额度用满时
`budget_total_requests_exhausted:55/55` 如实拒绝、不补发;**体验链提前走完
(如 51/55)时运行 `tools/seal_budget_hel879.py --db <团队实例库>` 显式封账**
——真实用量 51 原样写入并保留(不伪造 55、不清零),此后该库任何类别任何
余量的 `acquire` 都拒绝 `budget_sealed:51/55`,跨重启、跨 backup API 归档/
快照恢复同样拒绝(S6 负例:51/55 提前收尾、误重启、归档恢复均拒绝;重复
seal 幂等不覆盖原标记;无解除封账代码路径)。封账动作顺序:`systemctl stop`
8360 实例 → `seal_budget_hel879.py` → `tools/release_snapshot_hel896.py`
一致性快照(封账标记随库一同归档)→ 归档(数据目录 + 哈希清单)交交付评论。
`budget_total` 永不手工清零。
3. **旧测试数据不会转成用户伴侣**:测试档案、聊天、生活数据只存在于团队实例库与
归档;用户实例是全新空库,由用户自己创建档案;产品无任何「测试库导入用户库」
@@ -0,0 +1,52 @@
{
"ok": true,
"code_commit": "c121c42e39174e0f627f129e7f7e5700f33acf26",
"tool_version": 2,
"checks": [
{
"name": "naive_online_copy_loses_wal_reproduced",
"ok": true,
"detail": "error=no such table: companion"
},
{
"name": "busy_checkpoint_state_reproduced",
"ok": true,
"detail": "checkpoint=(1, 9, 8) naive_snapshot_events=error=DatabaseError:database disk image is malformed"
},
{
"name": "fix1_busy_checkpoint_fails_no_archive",
"ok": true,
"detail": "error=yes leftovers=0"
},
{
"name": "fix1b_row_mismatch_fails_no_archive",
"ok": true,
"detail": "error=源/快照行数不一致,不打包:/tmp/multica-task-29494715/hel879-snapshot-sel artifacts=[]"
},
{
"name": "fix_verify_rejects_rows_match_false_legacy_package",
"ok": true,
"detail": "ok=False checks=[{'db': 'databases/companion.db', 'sha256_ok': True, 'rows_ok': False, 'counts': {'companion': 1, 'chat_message': 1, 'events': 0}, 'source_counts': {'companion': 1, 'chat_message': 1, 'events': 1}, 'rows_match_recorded': False, 'rejected_legacy_package': True}]"
},
{
"name": "fix2_online_mode_refuses_dir_with_sqlite",
"ok": true,
"detail": "在线模式只允许显式单库 backup API(--db);--file /tmp/multica-task-29494715/hel879-snapshot-selftest-uapq8_ua/instance/history_root 内"
},
{
"name": "fix2_stop_mode_embeds_sqlite_via_consistency_path",
"ok": true,
"detail": "embedded=['/tmp/multica-task-29494715/hel879-snapshot-selftest-uapq8_ua/instance/history_root/mem0_public_history.db']"
},
{
"name": "fix3_verify_maps_source_paths_and_passes",
"ok": true,
"detail": "ok=True commit=c121c42e39174e0f627f129e7f7e5700f33acf26 plan_entries=4"
},
{
"name": "backup_api_snapshot_complete",
"ok": true,
"detail": "{\"companion\": 1, \"chat_message\": 1, \"events\": 1}"
}
]
}
+886
View File
@@ -0,0 +1,886 @@
{
"head": "c121c42e39174e0f627f129e7f7e5700f33acf26",
"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 条件请求』无法由缓存取证区分,按保守口径记为下载。
@@ -1,5 +1,5 @@
{
"head": "04eff611e7d85e539d43717711172441420ac853",
"head": "c121c42e39174e0f627f129e7f7e5700f33acf26",
"no_provider_calls": true,
"temp_only": true,
"scenes": {
@@ -59,6 +59,20 @@
"after_restart": "budget_total_requests_exhausted:55/55",
"archive_after_backup_api": "budget_total_requests_exhausted:55/55"
},
"S6_early_seal": {
"plan_total": 51,
"used_at_seal": 51,
"marker": {
"requests": 51,
"tokens": 510,
"sealed_at": 1793042553
},
"next_after_seal": "budget_sealed:51/55(封账于 1793042553,真实用量保留,不伪造、不清零)",
"other_scene_after_seal": "budget_sealed:51/55(封账于 1793042553,真实用量保留,不伪造、不清零)",
"after_restart": "budget_sealed:51/55(封账于 1793042553,真实用量保留,不伪造、不清零)",
"after_archive_restore": "budget_sealed:51/55(封账于 1793042553,真实用量保留,不伪造、不清零)",
"idempotent_reseal": true
},
"S3_user_instance": {
"B_after_A": "allowed",
"C_after_A": "allowed",
@@ -103,6 +117,16 @@
"ok": true,
"detail": "restart=budget_total_requests_exhausted:55/55 archive=budget_total_requests_exhausted:55/55 used=55"
},
{
"name": "S6a_early_finish_51_seals_real_usage_no_fake_no_zero",
"ok": true,
"detail": "used=51 dialogue_next=budget_sealed:51/55(封账于 1793042553,真实用量保留,不伪造、不清零) semantic_next=budget_sealed:51/55(封账于 1793042553,真实用量保留,不伪造、不清零) after=51 dialogue_daily=19/60"
},
{
"name": "S6b_seal_survives_restart_and_archive_restore",
"ok": true,
"detail": "restart=budget_sealed:51/55(封账于 1793042553,真实用量保留,不伪造、不清零) archive=budget_sealed:51/55(封账于 1793042553,真实用量保留,不伪造、不清零) arch_used=51 marker={'requests': 51, 'tokens': 510, 'sealed_at': 1793042553}"
},
{
"name": "S3_user_instance_B_and_C_unblocked_after_A",
"ok": true,
+54
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio
import json
import logging
import time
from contextlib import aclosing
from ..contracts import LLMMessage
@@ -61,6 +62,50 @@ class PersistentBudget(BudgetGuard):
)
return row["v"] if row else 0
# ------------------------------------------------------------ 封账(HEL-879)
def sealed(self):
"""封账标记(None=未封)。体验链提前走完时的可执行关闭,跨重启/归档不失效。"""
row = self.store.query_one("SELECT value v FROM budget_total WHERE key='sealed'")
if not row or row["v"] != 1:
return None
def _val(key, default=0):
r = self.store.query_one(f"SELECT value v FROM budget_total WHERE key='{key}'")
return r["v"] if r else default
return {"requests": _val("sealed_requests"), "tokens": _val("sealed_tokens"),
"sealed_at": _val("sealed_at")}
def seal(self):
"""显式封账:把当前真实用量连同时间戳原子写入账本,此后一切 acquire 拒绝。
HEL-879:A(团队测试实例)在额度未耗尽(如 51/55)时体验链走完,没有可执行
的封账就会在重启/归档恢复后放行第 52 次。封账标记与用量同库持久——重启、
backup API 归档、快照恢复后同样生效;真实用量保持 51,不伪造 55、不清零。
幂等:已封账时保留原标记(含原时间戳),不覆盖。解除封账没有代码路径:
复检 = 新批准 + 新实例/新库(budget-package §6.2.4)。
"""
self._roll()
with self.store.conn:
self.store.execute("BEGIN IMMEDIATE")
existing = self.sealed()
if existing is not None:
return existing
marker = {"requests": self.total_requests_used(),
"tokens": self.total_tokens_used(),
"sealed_at": int(time.time())}
self.store.execute("INSERT INTO budget_total VALUES('sealed',1) "
"ON CONFLICT(key) DO UPDATE SET value=1")
self.store.execute("INSERT INTO budget_total VALUES('sealed_requests',?) "
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
(marker["requests"],))
self.store.execute("INSERT INTO budget_total VALUES('sealed_tokens',?) "
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
(marker["tokens"],))
self.store.execute("INSERT INTO budget_total VALUES('sealed_at',?) "
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
(marker["sealed_at"],))
return marker
def reserve(self, tokens, scene):
"""Legacy background-scene token reservation (95% stop), kept for compatibility."""
self._roll()
@@ -85,6 +130,15 @@ class PersistentBudget(BudgetGuard):
day = self.state.day
with self.store.conn:
self.store.execute("BEGIN IMMEDIATE")
# 封账最优先(HEL-879):已显式封账后,任何类别/任何余量都拒绝——
# 提前收尾(如 51/55)与误重启/归档恢复一样不放行。
sealed = self.sealed()
if sealed is not None:
cap = self.config.total_requests
suffix = f"/{cap}" if cap is not None else ""
raise BudgetStop(
f"budget_sealed:{sealed['requests']}{suffix}(封账于 "
f"{sealed['sealed_at']},真实用量保留,不伪造、不清零)")
row = self.store.query_one(
"SELECT coalesce(sum(used),0) n FROM token_usage WHERE day=?", (day,)
)
+11 -2
View File
@@ -34,6 +34,7 @@ import hashlib
import importlib.metadata as md
import json
import re
import sys
from datetime import datetime
from pathlib import Path
@@ -85,7 +86,15 @@ def cache_hits(name: str, version: str) -> list[dict]:
def main() -> int:
out_dir = REPO / "docs/rework/hel896/sdk-audit"
import subprocess as _sp
try:
head = _sp.run(["git", "rev-parse", "HEAD"], cwd=REPO, capture_output=True,
text=True, check=True).stdout.strip()
except Exception:
head = "unknown"
# HEL-879:head 动态取当前提交,不再硬编码旧父提交冒充最新 head;
# 可选参数改写证据目录(默认保持 HEL-896 历史目录不动,本卡证据写 hel879/)。
out_dir = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else 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)])}
@@ -215,7 +224,7 @@ def main() -> int:
top = {n: rows.get(n) for n in ("mem0ai", "qdrant-client", "fastembed")}
result = {
"head": "04eff611e7d85e539d43717711172441420ac853",
"head": head,
"scope": ("只读审计:3.10 固定栈安装元数据按 3.11 Linux markers+extras 求闭包,对照产品 3.11 venv 与本机 uv/pip 缓存;"
"这是依赖闭包清单,不是已完成的 cp311 解析或下载结果"),
"read_only": True, "zero_downloads_this_card": True,
+372 -88
View File
@@ -1,24 +1,40 @@
#!/usr/bin/env python3
"""HEL-896 · 发布归档一致性快照与回退证据(修复 WAL 丢失;不冒充 R6 完整备份)。
"""HEL-879 · 发布归档一致性快照与回退证据(HEL-896 有限收口版;不冒充 R6 完整备份)。
总管独立复审(HEL-896 附件 §4)复现:在线库只复制主 `.db` 会丢 WAL 里已提交的资料
(复现件连表都不在;SQLite backup API 则完整)。本工具给出发布卡可直接照抄的归档方式:
HEL-896 工具的总管独立复审发现四个失败边界,本版全部收口:
停服务 + `PRAGMA wal_checkpoint(TRUNCATE)` 后整目录归档(离线口径,推荐);或
在线用 SQLite backup API 逐库做一致性快照(在线兜底口径)。
1. checkpoint 返回值必须核对:`PRAGMA wal_checkpoint(TRUNCATE)` 返回 (busy, log,
checkpointed),busy≠0 即未完成——旧版照抄复制并在 manifest 记 rows_match=False
仍打包成功。本版 busy≠0 / 源表行数≠快照行数一律**明确失败**:CLI 非 0、不产生
可交付归档(tar 不生成,半成品目录清除)。「先停服」是操作前提,不是忽略实际
失败结果的理由。
2. 目录内真实 SQLite 走一致性路径:history_root/vector_root 等数据目录里的
`mem0_public_history.db`、qdrant 存储等 SQLite 文件按文件头魔数递归识别
(`semantic.py` 的 history_db_path 就在 history_root 内)。停服口径对它们逐个
checkpoint+复制+逐表核对;**在线口径只允许显式单库 --db(backup API),传入含
SQLite 的目录直接拒绝**——在线目录裸复制正是丢 WAL 的不安全口径。
3. verify 拒绝 rows_match=False 的旧包,并按源路径映射做回退演练:解包到隔离新
目录、实际打开每个库核对行数与哈希(对照 manifest 记录的源行数与快照行数,
两者必须相等且与解包实测一致),非只打开 tar 中快照自证;输出 rollback_plan
(源路径 → 解包恢复路径)。
4. 完整实例归档只支持停服/释放全部写入方;在线模式仅显式单库 backup API。
产物:`<out>/<name>/` 下的 `databases/`(快照库)、`files/`(其余数据目录原样)、
`MANIFEST.json`(每个文件的 SHA256、源路径、代码精确提交、逐表行数核对结果)、
`<name>.tar.gz`。`--verify` 对既有归档重算哈希并重开快照库核对行数,作为回退演练。
产物:`<out>/<name>/` 下的 `databases/`(一致性快照库,含目录内发现的 SQLite)、
`files/`(其余数据文件原样)、`MANIFEST.json`(逐文件 SHA256、源路径、代码精确
提交、逐表行数核对结果)、`<name>.tar.gz`。归档永不删除;本工具不实现 R6。
自测(`--selftest`,临时目录,零产品数据)复现两种口径的差异并证明修复,输出
`docs/rework/hel896/release-snapshot/selftest.json`。
自测(`--selftest`,临时目录,零产品数据)复现:旧版失败形状(busy checkpoint 仍
打包、rows_match=False 旧包 verify 仍 ok、在线目录含 SQLite)与本版修复,输出
`docs/rework/hel879/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]
--out archives/ # 停服口径(推荐,目录内 SQLite 一并一致性处理)
python3 tools/release_snapshot_hel896.py snapshot --name ... --db path/a.db \
--out archives/ --online # 在线口径:仅显式单库 backup API,含 SQLite 的
# --file 目录会被拒绝
python3 tools/release_snapshot_hel896.py verify --archive archives/hel8xx-release.tar.gz
"""
@@ -36,6 +52,12 @@ import time
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
SQLITE_MAGIC = b"SQLite format 3\x00"
TOOL_VERSION = 2
class SnapshotError(RuntimeError):
"""快照/验证明确失败:CLI 非 0,不形成可交付成功包。"""
def sha256(path: Path) -> str:
@@ -46,6 +68,26 @@ def sha256(path: Path) -> str:
return h.hexdigest()
def is_sqlite(path: Path) -> bool:
"""按文件头魔数识别真实 SQLite 库(覆盖 .db/.sqlite3/无后缀;qdrant 本地存储)。"""
try:
with open(path, "rb") as f:
return f.read(16) == SQLITE_MAGIC
except OSError:
return False
def find_sqlite(root: Path) -> list[Path]:
"""递归找出目录内全部真实 SQLite 文件(按魔数,不按扩展名)。"""
if root.is_file():
return [root] if is_sqlite(root) else []
found: list[Path] = []
for p in sorted(root.rglob("*")):
if p.is_file() and is_sqlite(p):
found.append(p)
return found
def table_counts(db: Path) -> dict[str, int]:
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
try:
@@ -56,18 +98,25 @@ def table_counts(db: Path) -> dict[str, int]:
con.close()
def checkpoint(db: Path) -> None:
def checkpoint(db: Path) -> tuple[int, int, int]:
"""TRUNCATE checkpoint,返回 (busy, log, checkpointed)。busy≠0 = 未完成。"""
con = sqlite3.connect(str(db))
try:
con.execute("PRAGMA wal_checkpoint(TRUNCATE)")
row = con.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone()
return (row[0], row[1], row[2]) if row else (1, -1, -1)
finally:
con.close()
def snapshot_db(src: Path, dst: Path, *, online: bool) -> str:
"""一致性快照单库。online=False 先 checkpoint(停服口径);True 用 backup API。"""
"""一致性快照单库。online=False 先 checkpoint(必须完成);True 用 backup API。"""
if not online:
checkpoint(src)
busy, log, ckpt = checkpoint(src)
if busy != 0:
raise SnapshotError(
f"checkpoint 未完成(busy={busy}, log={log}, checkpointed={ckpt}):"
f"{src} 仍有读取/写入方持有 WAL。停服口径要求先停止全部相关写入方,"
f"不得忽略该失败继续打包。")
shutil.copy2(src, dst)
return "stop+checkpoint+copy"
with sqlite3.connect(str(src)) as source, sqlite3.connect(str(dst)) as target:
@@ -84,61 +133,135 @@ def git_head() -> str:
def make_snapshot(name: str, dbs: list[Path], files: list[Path], out_dir: Path, *, online: bool) -> dict:
"""做一致性快照。任一环节失败:不产生 tar、清除半成品、抛 SnapshotError。
收口语义(HEL-879):
- 全部 --db 与目录内发现的 SQLite 都走一致性路径并逐表核对源/快照行数;
- 行数不一致或 checkpoint busy = 明确失败(旧版记 rows_match=False 仍打包);
- 在线模式含 SQLite 的 --file 目录直接拒绝(在线目录裸复制丢 WAL)。
"""
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",
manifest: dict = {"name": name, "created": stamp, "code_commit": git_head(),
"tool": "release_snapshot_hel896.py", "tool_version": TOOL_VERSION,
"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
try:
(root / "databases").mkdir(parents=True, exist_ok=True)
for db in dbs:
if not Path(db).is_file():
raise SnapshotError(f"--db 不是文件:{db}")
dst = root / "databases" / db.name
method = snapshot_db(Path(db), dst, online=online)
src_counts = table_counts(Path(db))
dst_counts = table_counts(dst)
if src_counts != dst_counts:
raise SnapshotError(
f"源/快照行数不一致,不打包:{db} source={src_counts} snapshot={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": True})
for f in files:
src = Path(f)
if not src.exists():
raise SnapshotError(f"--file 不存在:{src}")
embedded = find_sqlite(src)
if online and embedded:
raise SnapshotError(
f"在线模式只允许显式单库 backup API(--db);--file {src} 内含 SQLite:"
f"{[str(e) for e in embedded]}。含活动库的目录在线裸复制会丢 WAL,"
f"拒绝。完整实例归档请用停服口径(默认模式)。")
for emb in embedded:
# 停服口径:目录内真实 SQLite 一并走 checkpoint+复制+逐表核对。
rel = emb.relative_to(src) if emb.is_relative_to(src) else Path(emb.name)
dst = root / "databases" / f"{src.name}__{rel}".replace("/", "__")
method = snapshot_db(emb, dst, online=False)
src_counts = table_counts(emb)
dst_counts = table_counts(dst)
if src_counts != dst_counts:
raise SnapshotError(
f"目录内 SQLite 源/快照行数不一致,不打包:{emb} "
f"source={src_counts} snapshot={dst_counts}")
manifest["databases"].append({"source": str(emb), "snapshot": str(dst.relative_to(root)),
"sha256": sha256(dst), "method": method + "+dir-scan",
"source_table_counts": src_counts,
"snapshot_table_counts": dst_counts,
"rows_match": True})
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})
if not manifest["databases"]:
raise SnapshotError("没有任何数据库进入一致性路径(--db 为空)")
manifest["verify"] = "rows_match=True (all, enforced; mismatch aborts packaging)"
(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
except Exception:
# 失败不形成可交付包:清除半成品目录与可能已生成的 tar。
shutil.rmtree(root, ignore_errors=True)
for partial in out_dir.glob(f"{name}-{stamp}.tar.gz"):
partial.unlink(missing_ok=True)
raise
def verify_archive(archive: Path) -> dict:
"""回退演练:隔离新目录解包,实际打开各库核对行数与哈希,按源路径映射。
收口语义(HEL-879):
- manifest 记录 rows_match=False(旧版失败包)→ ok=False 拒绝;
- 行数同时对照 source_table_counts 与 snapshot_table_counts:两者必须相等,
且与解包实测一致(旧版只比快照自证,源=1 行/快照=0 行的漏行包仍 ok)。
"""
result: dict = {"archive": str(archive), "sha256": sha256(archive), "checks": []}
with tempfile.TemporaryDirectory(prefix="hel896-verify-") as tmp:
with tempfile.TemporaryDirectory(prefix="hel879-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")
result["tool_version"] = manifest.get("tool_version", 1)
ok_all = True
rollback_plan = []
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")
try:
counts = table_counts(snap)
except sqlite3.Error as exc:
result["checks"].append({"db": d["snapshot"], "sha256_ok": digest == d["sha256"],
"rows_ok": False, "error": str(exc),
"rows_match_recorded": d.get("rows_match")})
ok_all = False
continue
recorded_rows_match = d.get("rows_match")
src_counts = d.get("source_table_counts")
snap_counts = d.get("snapshot_table_counts")
rows_ok = counts == snap_counts and (
recorded_rows_match is True or (src_counts is not None and counts == src_counts))
legacy_reject = recorded_rows_match is False or (
recorded_rows_match is None and src_counts != snap_counts)
ok = digest == d["sha256"] and rows_ok and not legacy_reject
ok_all &= ok
result["checks"].append({"db": d["snapshot"], "sha256_ok": digest == d["sha256"],
"rows_ok": counts == d.get("snapshot_table_counts"),
"counts": counts})
"rows_ok": rows_ok, "counts": counts,
"source_counts": src_counts,
"rows_match_recorded": recorded_rows_match,
"rejected_legacy_package": legacy_reject})
rollback_plan.append({"source": d.get("source"), "restored_path": str(snap),
"restore_action": "停服后复制回源路径(行数与哈希已在本演练核对)"})
for f in manifest["files"]:
base = root / f["snapshot"]
for rel, want in f["entries"].items():
@@ -146,26 +269,36 @@ def verify_archive(archive: Path) -> dict:
ok = sha256(p) == want
ok_all &= ok
result["checks"].append({"file": str(p.relative_to(root)), "sha256_ok": ok})
rollback_plan.append({"source": f.get("source"), "restored_path": str(base),
"restore_action": "停服后按相对结构复制回源目录"})
result["rollback_plan"] = rollback_plan
result["ok"] = ok_all
return result
def _make_wal_db(path: Path) -> sqlite3.Connection:
path.parent.mkdir(parents=True, exist_ok=True)
con = sqlite3.connect(str(path))
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.execute("CREATE TABLE events(id INTEGER PRIMARY KEY, kind TEXT)")
con.commit()
con.execute("INSERT INTO companion VALUES(1,'her')")
con.execute("INSERT INTO chat_message VALUES(1,'hello')")
con.commit()
return con
def selftest(out_dir: Path) -> int:
"""临时库复现:在线裸复制主 .db 丢 WAL 已提交行;checkpoint 与 backup API 都完整。"""
"""临时库复现全部失败边界并证明修复(零产品数据、零网络)。"""
checks: list[dict] = []
with tempfile.TemporaryDirectory(prefix="hel896-snapshot-selftest-") as tmp:
with tempfile.TemporaryDirectory(prefix="hel879-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。
con = _make_wal_db(db)
# ── 旧失败形状 1 复现:WAL 持有(读者未放)时 naive 复制丢已提交行。 ──
naive = tmp / "naive-copy.db"
shutil.copy2(db, naive)
try:
@@ -174,37 +307,180 @@ def selftest(out_dir: Path) -> int:
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:
checks.append({"name": "naive_online_copy_loses_wal_reproduced",
"ok": "rows=1" not in naive_result, "detail": naive_result})
# ── 旧失败形状 2 复现(总管 edges 场景):读者先持读事务,随后写入方提交
# events=1;TRUNCATE checkpoint 返回 busy=[1,1,0],naive 快照丢该行,
# 旧工具此时仍打包成功。 ──
reader = sqlite3.connect(str(db))
reader.execute("BEGIN")
reader.execute("SELECT count(*) FROM companion").fetchone()
con.execute("INSERT INTO events VALUES(1,'reach_out')")
con.commit()
busy_state = checkpoint(db)
naive2 = tmp / "naive2.db"
shutil.copy2(db, naive2)
try:
naive2_events = sqlite3.connect(f"file:{naive2}?mode=ro", uri=True).execute(
"SELECT count(*) FROM events").fetchone()[0]
except sqlite3.Error as exc:
naive2_events = f"error={type(exc).__name__}:{exc}"
checks.append({"name": "busy_checkpoint_state_reproduced",
"ok": busy_state[0] != 0 and naive2_events != 1,
"detail": f"checkpoint={busy_state} naive_snapshot_events={naive2_events}"})
# ── 修复 1:busy checkpoint 必须明确失败,不形成归档。 ──
out_busy = tmp / "archives-busy"
out_busy.mkdir()
failed_busy = None
try:
make_snapshot("busy-must-fail", [db], [], out_busy, online=False)
except SnapshotError as exc:
failed_busy = str(exc)
leftovers = list(out_busy.iterdir())
checks.append({"name": "fix1_busy_checkpoint_fails_no_archive",
"ok": failed_busy is not None and "busy" in failed_busy and not leftovers,
"detail": f"error={'yes' if failed_busy else 'none'} leftovers={len(leftovers)}"})
reader.rollback()
reader.close()
# ── 修复 1b:源/快照行数不一致(快照停在某刻、源继续前进的形状)必须
# 明确失败、不形成归档。用停当快照替换法直接触发比对分支。 ──
out_mis = tmp / "archives-mismatch"
out_mis.mkdir()
stale = out_mis / "stale.db"
with sqlite3.connect(str(db)) as source, sqlite3.connect(str(stale)) 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.execute("INSERT INTO events VALUES(2,'after')")
con.commit()
real_snapshot_db = snapshot_db
_self = globals()
def _stale_snapshot(src: Path, dst: Path, *, online: bool) -> str:
shutil.copy2(stale, dst)
return "stop+checkpoint+copy(stale-standin)"
_self["snapshot_db"] = _stale_snapshot
failed_mis = None
try:
make_snapshot("mismatch-must-fail", [db], [], out_mis, online=False)
except SnapshotError as exc:
failed_mis = str(exc)
finally:
_self["snapshot_db"] = real_snapshot_db
mis_leftovers = list(out_mis.glob("mismatch-must-fail*"))
checks.append({"name": "fix1b_row_mismatch_fails_no_archive",
"ok": failed_mis is not None and "不一致" in failed_mis
and not [p for p in mis_leftovers if p.suffix == ".gz"],
"detail": f"error={failed_mis[:60] if failed_mis else 'none'} "
f"artifacts={[p.name for p in mis_leftovers]}"})
con.execute("DELETE FROM events WHERE id=2")
con.commit()
# ── 旧失败形状 3 复现:手工构造 rows_match=False 的旧包,旧版 verify 仍 ok。 ──
legacy_root = tmp / "legacy-pkg"
legacy_root.mkdir()
(legacy_root / "databases").mkdir()
legacy_snap = legacy_root / "databases" / "companion.db"
with sqlite3.connect(str(db)) as source, sqlite3.connect(str(legacy_snap)) as target:
source.backup(target)
# 漏行形状:快照内删掉 events 行,manifest 记 rows_match=False(旧版真实输出)。
with sqlite3.connect(str(legacy_snap)) as lk:
lk.execute("DELETE FROM events")
legacy_manifest = {
"name": "legacy", "code_commit": "legacy-shape", "tool": "release_snapshot_hel896.py",
"tool_version": 1, "mode": "stop-checkpoint-copy",
"databases": [{"source": str(db), "snapshot": "databases/companion.db",
"sha256": sha256(legacy_snap), "method": "stop+checkpoint+copy",
"source_table_counts": {"companion": 1, "chat_message": 1, "events": 1},
"snapshot_table_counts": {"companion": 1, "chat_message": 1, "events": 0},
"rows_match": False}],
"files": [], "verify": "rows_match=False",
}
(legacy_root / "MANIFEST.json").write_text(
json.dumps(legacy_manifest, ensure_ascii=False), encoding="utf-8")
legacy_tar = tmp / "legacy-pkg.tar.gz"
with tarfile.open(legacy_tar, "w:gz") as tf:
tf.add(legacy_root, arcname=legacy_root.name)
legacy_verify = verify_archive(legacy_tar)
checks.append({"name": "fix_verify_rejects_rows_match_false_legacy_package",
"ok": legacy_verify["ok"] is False,
"detail": f"ok={legacy_verify['ok']} checks={legacy_verify['checks'][:1]}"})
# ── 修复 2:目录内 SQLite(history_root 形状)。 ──
hist = tmp / "instance" / "history_root"
hist.mkdir(parents=True, exist_ok=True)
mem_hist = hist / "mem0_public_history.db"
mem_con = sqlite3.connect(str(mem_hist))
mem_con.execute("PRAGMA journal_mode=WAL")
mem_con.execute("CREATE TABLE history(id INTEGER PRIMARY KEY, payload TEXT)")
mem_con.execute("INSERT INTO history VALUES(1,'mem')")
mem_con.commit()
mem_reader = sqlite3.connect(str(mem_hist))
mem_reader.execute("BEGIN")
mem_reader.execute("SELECT count(*) FROM history").fetchone()
# 在线模式 + 含 SQLite 的目录 → 拒绝。
out_online = tmp / "archives-online"
out_online.mkdir()
refused = None
try:
make_snapshot("online-dir-refused", [db], [hist], out_online, online=True)
except SnapshotError as exc:
refused = str(exc)
checks.append({"name": "fix2_online_mode_refuses_dir_with_sqlite",
"ok": refused is not None and "拒绝" in refused and not list(out_online.iterdir()),
"detail": refused[:120] if refused else "not refused"})
mem_reader.rollback()
mem_reader.close()
# 停服口径:目录内 SQLite 走 checkpoint+复制+逐表核对(mem_reader 已放,
# 但 mem_con 仍持有 WAL——checkpoint 必须真实完成才算过)。
mem_con.close()
# 主库也关闭,进入停服状态。
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")
vec = tmp / "instance" / "vec"
vec.mkdir(exist_ok=True)
(vec / "vectors.bin").write_bytes(b"\x00\x01\x02cg")
out = tmp / "archives"
out.mkdir()
manifest = make_snapshot("hel896-selftest", [db], [sem_dir], out, online=False)
manifest = make_snapshot("hel879-selftest", [db], [vec, hist], out, online=False)
# 从解包侧核对 manifest(目录内 SQLite 是否真走了逐表核对)。
with tarfile.open(manifest["archive"]) as tf:
tf.extractall(out / "unpack")
unpack_root = out / "unpack" / Path(manifest["archive"]).name.replace(".tar.gz", "")
unpack_manifest = json.loads((unpack_root / "MANIFEST.json").read_text(encoding="utf-8"))
embedded_dbs = [d for d in unpack_manifest["databases"] if "+dir-scan" in d["method"]]
hist_ok = any("mem0_public_history.db" in d["source"] and d["rows_match"] is True
and d["source_table_counts"].get("history") == 1 for d in embedded_dbs)
checks.append({"name": "fix2_stop_mode_embeds_sqlite_via_consistency_path",
"ok": bool(embedded_dbs) and hist_ok,
"detail": f"embedded={[d['source'] for d in embedded_dbs]}"})
# ── 修复 3:回退演练按源路径映射、实测行数与哈希、旧包拒绝已验。 ──
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'])}"})
plan_sources = {entry.get("source") for entry in verify.get("rollback_plan", [])}
mapped = str(db) in plan_sources and any(
"mem0_public_history.db" in (s or "") for s in plan_sources)
checks.append({"name": "fix3_verify_maps_source_paths_and_passes",
"ok": verify["ok"] and mapped,
"detail": f"ok={verify['ok']} commit={verify.get('code_commit')} "
f"plan_entries={len(verify.get('rollback_plan', []))}"})
# backup API(在线口径,仅显式单库)依旧完整。
with sqlite3.connect(str(db)) as source, sqlite3.connect(str(tmp / "via-api.db")) as target:
source.backup(target)
api_counts = table_counts(tmp / "via-api.db")
checks.append({"name": "backup_api_snapshot_complete",
"ok": api_counts.get("companion") == 1 and api_counts.get("chat_message") == 1,
"detail": json.dumps(api_counts)})
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),
json.dumps({"ok": ok, "code_commit": git_head(),
"tool_version": TOOL_VERSION, "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']}")
@@ -220,15 +496,23 @@ def main() -> int:
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("--online", action="store_true",
help="在线口径:仅显式单库 backup API(默认停服+checkpoint;含 SQLite 的 --file 目录会被拒绝)")
ap.add_argument("--archive", type=Path)
args = ap.parse_args()
if args.selftest:
return selftest(REPO / "docs/rework/hel896/release-snapshot")
return selftest(REPO / "docs/rework/hel879/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)
out_dir = args.out
out_dir.mkdir(parents=True, exist_ok=True)
try:
manifest = make_snapshot(args.name, args.db, args.file, out_dir, online=args.online)
except SnapshotError as exc:
print(json.dumps({"ok": False, "error": str(exc),
"deliverable_archive": None}, ensure_ascii=False, indent=1))
return 1
print(json.dumps({k: manifest[k] for k in ("archive", "archive_sha256", "code_commit", "mode", "verify")},
ensure_ascii=False, indent=1))
return 0
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""HEL-879 · A(团队测试实例)显式封账工具——体验链提前走完时的可执行关闭授权。
总管独立复审复现的失败边界:临时库使用 51/55 → 停服/backup 归档 → 原配置重开
后第 52 次仍 allowed——「走完体验链」与「耗尽 55」并不等价,没有可执行封账。
本工具把关闭授权落成一个显式、可审计的动作(budget-package-hel895.md §6.2):
python3 tools/seal_budget_hel879.py --db /opt/<8360实例>/data/<产品库>.sqlite3 [--reason "体验链走完"]
行为:
- 在该库 `budget_total` 原子写入 sealed/sealed_requests/sealed_tokens/sealed_at;
- 真实用量照实保留(如 51),不伪造 55、不清零、不改其他行;
- 此后该库任何 `PersistentBudget.acquire` 在发送前抛 `budget_sealed:<used>/<cap>`,
跨重启、跨 backup API 归档/快照恢复同样拒绝(负例见
`tools/verify_budget_split_hel896.py` S6);
- 无解除封账的代码路径:团队复检 = 新批准 + 新实例/新库(§6.2.4)。
封账顺序:先停服 → 跑本工具 → `tools/release_snapshot_hel896.py` 一致性快照
(封账标记随库一同归档)。本工具只写上述 4 行标记,零 provider 调用、零网络。
"""
from __future__ import annotations
import argparse
import json
import sys
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
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--db", required=True, type=Path, help="团队实例产品库路径")
ap.add_argument("--reason", default="", help="封账原因(记录在交付评论,不写入库)")
args = ap.parse_args()
if not args.db.is_file():
print(json.dumps({"ok": False, "error": f"库不存在:{args.db}"}, ensure_ascii=False))
return 1
store = Store(args.db, init=False)
guard = PersistentBudget(store, BudgetGuard(BudgetConfig()))
before = guard.total_requests_used()
marker = guard.seal()
# 自检:封账后 acquire 必须被拒且不落账(拒绝路径在任何写入之前)。
try:
guard.acquire("dialogue_main", tokens=1)
rejected, message = False, "allowed(封账失效!)"
except BudgetStop as exc:
rejected, message = True, str(exc)
after = guard.total_requests_used()
ok = (marker["requests"] == before == after and rejected
and message.startswith(f"budget_sealed:{marker['requests']}"))
print(json.dumps({"ok": ok, "db": str(args.db), "reason": args.reason,
"sealed_marker": marker, "real_usage_preserved": before == after,
"post_seal_acquire": message}, ensure_ascii=False, indent=1))
store.close()
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())
+72 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""HEL-896 · A/B/C 分账与阶段切换验证(仅临时库 + 替身,零 provider、零网络)。
"""HEL-896/879 · A/B/C 分账、阶段切换与提前封账验证(仅临时库 + 替身,零 provider、零网络)。
总管独立复审(HEL-896 附件 §1)复现的问题:把 A 的一次性总量 55 写进与 B/C 共用的
产品库 PersistentBudget 后,B 的 mem0_extract 与 C 的 dialogue_main 都被
@@ -12,6 +12,9 @@ budget_total_requests_exhausted:55/55 挡住,且 budget_total/request_usage/to
S2 封账 —— 重启不归零;SQLite backup API 一致性快照归档后,归档件
仍是 55/55 且继续拒绝(封账跨重启、跨归档成立);测试档案
只留在团队库;
S6 提前封账(HEL-879) —— 体验链 51/55 走完即显式 seal:真实用量保留 51(不伪造
55、不清零),第 52 次任何类别拒绝 budget_sealed:51/55;
误重启、backup API 归档恢复后同样拒绝;seal 幂等不覆盖;
S3 全新用户实例(B/C) —— 独立新库、不配置一次性总量:A 耗尽后 B(mem0_extract)
与 C(dialogue_main)照常放行;日顶叠加(dialogue 60、
semantic 40、daily_tokens)逐项到顶拒绝;拒绝不落账;
@@ -32,6 +35,7 @@ from __future__ import annotations
import hashlib
import json
import sqlite3
import subprocess
import sys
import tempfile
from pathlib import Path
@@ -42,7 +46,18 @@ 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"
REPO = Path(__file__).resolve().parents[1]
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"
HEAD = git_head()
RESULTS: list[tuple[str, bool, str]] = []
@@ -150,6 +165,61 @@ def main() -> int:
team_reopened.close()
sealed.close()
# ── S6:提前封账(HEL-879)——体验链 51/55 走完即 seal,误重启/归档恢复均拒绝 ──
early = Store(tmp / "team-8360-early.db")
early_cfg = BudgetConfig(daily_tokens=2_000_000,
daily_requests={"dialogue": 60, "life": 8, "background": 40, "semantic": 40},
total_requests=55, total_tokens=500_000)
early_guard = PersistentBudget(early, BudgetGuard(early_cfg))
# A 表 51 次(创建/诠释/习惯4 + 对话16 + 生活8 + 判定/成长/gist20 + 主动措辞3)
# 就是体验链全部内容——走完即停,不补到 55。
early_plan = [("fact_extract", 4), ("dialogue_main", 16), ("life_event", 8),
("memory_gist", 20), ("proactive_message", 3)]
early_ok = True
for scene, n in early_plan:
for _ in range(n):
early_ok &= acquire(early_guard, scene) == "allowed"
used_before_seal = early_guard.total_requests_used()
marker = early_guard.seal()
s6_52nd = acquire(early_guard, "dialogue_main")
s6_other_scene = acquire(early_guard, "mem0_extract")
s6_daily_still_has_room = early_guard.requests_used("dialogue") # 19/60:日顶有余
check("S6a_early_finish_51_seals_real_usage_no_fake_no_zero",
early_ok and used_before_seal == 51 and marker["requests"] == 51
and s6_52nd.startswith("budget_sealed:51/55") and s6_other_scene.startswith("budget_sealed:51/55")
and early_guard.total_requests_used() == 51 and s6_daily_still_has_room == 19,
f"used={used_before_seal} dialogue_next={s6_52nd} semantic_next={s6_other_scene} "
f"after={early_guard.total_requests_used()} dialogue_daily={s6_daily_still_has_room}/60")
# seal 幂等:重复 seal 不覆盖原标记(sealed_at 不变)。
again = early_guard.seal()
idempotent = again == marker
# 误重启:重开库后第 52 次仍拒绝。
early.close()
early_reopened = Store(tmp / "team-8360-early.db", init=False)
early_guard2 = PersistentBudget(early_reopened, BudgetGuard(early_cfg))
s6_after_restart = acquire(early_guard2, "dialogue_main")
# 归档恢复:backup API 归档封账库 → 打开归档件 → 同样拒绝,用量仍 51。
early_archive = tmp / "team-8360-early-sealed.db"
snapshot_via_backup_api(tmp / "team-8360-early.db", early_archive)
early_arch = Store(early_archive, init=False)
early_arch_guard = PersistentBudget(early_arch, BudgetGuard(early_cfg))
s6_after_archive = acquire(early_arch_guard, "dialogue_main")
arch_marker = early_arch_guard.sealed()
check("S6b_seal_survives_restart_and_archive_restore",
idempotent and s6_after_restart.startswith("budget_sealed:51/55")
and s6_after_archive.startswith("budget_sealed:51/55")
and early_arch_guard.total_requests_used() == 51
and arch_marker is not None and arch_marker["requests"] == 51,
f"restart={s6_after_restart} archive={s6_after_archive} "
f"arch_used={early_arch_guard.total_requests_used()} marker={arch_marker}")
report["scenes"]["S6_early_seal"] = {
"plan_total": sum(n for _, n in early_plan), "used_at_seal": used_before_seal,
"marker": marker, "next_after_seal": s6_52nd, "other_scene_after_seal": s6_other_scene,
"after_restart": s6_after_restart, "after_archive_restore": s6_after_archive,
"idempotent_reseal": idempotent}
early_reopened.close()
early_arch.close()
# ── S3:全新用户实例(B/C)——独立新库、无一次性总量:A 耗尽后 B/C 照常放行,日顶叠加 ──
user = Store(tmp / "user-fresh.db")
user_cfg = BudgetConfig(daily_tokens=2_000_000,