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:
co-authored by
multica-agent
parent
c121c42e39
commit
01ff919859
@@ -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
|
||||
|
||||
@@ -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. 本候选不做的事
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 条件请求』无法由缓存取证区分,按保守口径记为下载。"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,)
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user