HEL-885 rework: per-row progress matches what was really sent, cross-day and unknown-outcome accounting stay conservative, positive caps require the ledger authority, and the release dependency-fill command actually exists - every Memory.add now carries an anchor for exactly its own row (the HEL-882 whole-batch anchor let the first stored vector mark unsent rows complete; the manager's real-SDK partial reproduction showed the user's original words never leaving the process), a row finishing with a legitimately EMPTY extraction gets a durable sem_row_done record (r4.sql additive table) written inside the worker thread the moment its add completes, resume skips = vector-store anchors ∪ sem_row_done so no row is lost, none falsely complete, and finished work is never re-sent across budget-refusal/crash/restart/lease/concurrency, entry_ids merge across partial attempts, and the swallowed-failure detection is per-row; PersistentBudget.settle/release book on hold.day (the old write to state.day manufactured -80 used on the new day and re-opened headroom the reservation day had already spent; token_usage/request_usage refund the paying day, budget_total stays global) and the empty-budget_total TypeError crash in total_requests_used/total_tokens_used is fixed - the old one-shot-total test passed only because that crash surfaced as chat.error, it now books the total honestly; _atomic_reserve no longer purges aged reserve: rows (a crashed worker's hold is an UNKNOWN outcome - an hour-old timeout is not proof the call never happened; unknown consumption stays billed across restarts, only a provably-unsent release() ever removes a hold, embed_ops and llm_ops share the fix); Mem0Backend refuses construction before importing mem0 when any cap>0 lacks a (non-empty dict) authority block - ledger_authority_required - or when the prefix check fails, closing the fresh-empty-ledger bypass the manager reproduced live, config/semantic.example.yaml ships the authority block as REQUIRED with the explicit cumulative formula (embed_cap=500+approved-increment e.g. 680, embed_reserve=2 frozen untouched, min_used=498; never write 180 as the cumulative cap of a 498-used account) mirrored into docs/budget-plan-hel874.md §2 with the HEL-885 accounting rules; deploy/hel874-release.md §4 replaces the nonexistent uv pip download (rc=2, re-verified) with python3 -m pip download --only-binary :all: --platform manylinux2014_x86_64 --python-version 3.11 --implementation cp --abi cp311 + sha256 manifest + uv pip install --no-index --find-links offline merge + same-process co-import verdict, honest missing-materials list unchanged (no 3.11 wheels cached, zero downloads this card, no version swaps), and tools/check_release_offline_hel882.py gains R7 probing both command surfaces offline; verification: 281 python tests pass on 3.11.16 venv + requirements.lock offline (9 new tests/test_rework_hel885.py cases: partial batch claims only the sent row / empty-extraction row completes and is never resent / process restart sends only the missing row / concurrent ticks never double-send / cross-day settle and release book on the reservation day / aged unknown llm+embed reservations never refunded / missing-empty-malformed-wrong authority all fail closed; test_resume_never_loses_or_duplicates_rows rewritten to drive the REAL product path instead of hand-planting single-row anchors the product never wrote), ruff clean (src/tests/tools), web tsc + 42 vitest + build via npm ci --offline, pinned mem0 2.2.1 install on the deterministic boundary: contract re-run 12/12 (harness now hands a synthetic authority prefix - production defaults not relaxed), new tools/verify_semantic_recovery_hel885.py 7/7 (S0 authority fail-closed, S1 per-row partial with exactly 1 HTTP, S2 restart sends only the missing row proven by HTTP count since mem0 prompts legitimately carry old rows in rolling history, S3 lease recovery + concurrent ticks single-send, S4a/S4b unknown reservations kept), both manager review scripts re-run VERBATIM against this head (partial: completed=[2] only, resume done with 2 entries, HTTP=2, user words actually sent; extra: new-day used=0, over-cap request refused, aged reservation second_allowed=false, uv probe still rc=2), release offline check R1-R7 green, dual-target requirement checks 4/4 with handoff-HEL-885.json; zero real LLM, zero real embeddings, zero non-loopback outbound, authoritative ledgers and freezes untouched, not deployed, PR not merged, no visual changes

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
zonggong
2026-10-27 00:01:44 +08:00
co-authored by multica-agent
parent 59fb06304f
commit 78b6267d3a
23 changed files with 1603 additions and 111 deletions
+19 -8
View File
@@ -15,20 +15,31 @@
# 嵌入账本:沿用 HEL-857 系列唯一权威账(embed_ops 表)续账,绝不另起新账;
# SDK 内部提取 LLM 记同库 llm_ops 新表(独立 cap,互不挪用)
#
# ── 累计上限算式(HEL-885,写入前照此换算,禁止直接填增量)───────────────
# 权威账现状:已用 498 条 / 原批上限 500(其中冻结不动 2 条余额)。
# 白栖知批准的 B 部分新增嵌入额度(例如 180 条)并入同一累计账:
# embed_cap = 500 + 180 = 680 # 原上限 + 新批增量(不是 180!)
# embed_reserve = 2 # 原冻结余额 2 条继续保留不动
# 可用余量 = 680 − 498 − 2 = 180
# 错误示例:embed_cap: 180(对已用 498 的账等于瞬间耗尽并掩盖历史);
# 动到旧冻结 2 条(min_used: 498 会拒绝)。
ledger_path: /absolute/path/to/embed_ledger.sqlite3
embed_cap: 0 # 本产品环境允许使用的嵌入条数累计上限;0=未批准=全部拒绝
embed_daily: 0 # 嵌入条数日顶(执行表 C:建议 60/日,待批);0=未批准=拒绝
embed_reserve: 0 # 预留不动的余量(例如为复测保留 40)
embed_reserve: 0 # 预留不动的余量(例如为复测保留 40;冻结口径见上方算式)
llm_cap: 0 # Mem0 内部提取 LLM 调用累计上限(执行表 B:建议 60,待批)
llm_daily: 0 # Mem0 内部提取 LLM 日顶(执行表 C:建议 40/日,待批)
# 冻结前缀(HEL-882 防绕过):任一 cap>0 时,ledger_path 必须仍是含冻结前缀的
# 权威账——指向新空文件重开余额会被 verify_ledger_authority 拒绝。数值由总管
# 按权威账现状填写(前 N 行逐行 SHA256 与最少已用条数)。
# authority:
# prefix_rows: 181
# prefix_sha256: "<权威账前 N 行逐行摘要>"
# min_used: 498
# 冻结前缀(HEL-885 起为必填,不可注释掉):只要 embed_cap / llm_cap 任一 >0,
# 缺少、留空或填错的 authority 块都会让 mem0 模式在构造前直接拒绝
# (ledger_authority_required / ledger_authority_*,fail-closed,不降级)。
# ledger_path 必须仍是含冻结前缀的权威账——指向新空文件重开余额同样被拒。
# 数值由总管按权威账现状填写(前 N 行逐行 SHA256 与最少已用条数):
authority:
prefix_rows: 181
prefix_sha256: "<权威账前 N 行逐行摘要,总管填写>"
min_used: 498
# 记录作用域:add(user_id=) 与 search(filters={"user_id":…}) 必须一致;
# mem0 2.2.1 拒绝无过滤 search,此键一般不需要改
+37 -15
View File
@@ -66,39 +66,61 @@ Mem0 提取都会在发送前被硬闸拒绝(`budget_*_exhausted`)——这
**当前事实(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
的 3.11 wheel,本卡不下载。因此发布卡要在 **B 部分批准后** 先落 wheel 再激活:
的 3.11 wheel,本卡不下载。因此发布卡要在 **B 部分批准后** 先落 wheel 再激活。
**HEL-885 命令勘误:旧版此处的 `uv pip download …` 在本机 uv(0.13.5)不存在
(`uv pip download --help` 退出 2:unrecognized subcommand 'download',已复测,
见 docs/rework/hel885/)。本机确实可用的落 wheel 工具是 pip 的 download 子命令
(`python3 -m pip download --help` 退出 0)。发布 venv 由 uv 创建、不带 pip,
这不影响:下载在任一装有 pip 的 Python 上执行即可,wheel 按目标平台解析。**
```bash
# 4.1 在有内网源权限的环境把固定栈拉全(发布卡执行并留证;不在本卡执行)
# mem0ai==2.2.1(源码 2c81af3,tarball SHA256 9dcef930…dda23)及其依赖:
# qdrant-client(1.20.x)、fastembed(0.9.x)、openai、pytz、sqlmodel 等
# 4.1 在有内网源权限的环境把固定栈拉全(发布卡执行并留证;不在本卡执行、
# 不下载)。先核对目标机平台:uname -m 必须与 --platform 一致(当前
# 部署机为 x86_64 Linux)。
uname -m # 预期 x86_64;不符则改 --platform
mkdir -p /opt/cybergril-hel874/wheels
uv pip download --python-version 3.11 -r deploy/mem0-stack.lock -d /opt/cybergril-hel874/wheels
# 4.2 同一发布进程离线并入(一个 venv 同时 import 产品与 mem0 —— 唯一被认可形态)
python3 -m pip download -r deploy/mem0-stack.lock -d /opt/cybergril-hel874/wheels \
--only-binary :all: --implementation cp --python-version 3.11 \
--platform manylinux2014_x86_64 --abi cp311
# 4.1b 固定源身份与哈希留证:下载当时逐 wheel 记录 sha256(与发布评论一并
# 存档;mem0ai 的源码身份 2c81af3 / tarball SHA256 见 deploy/mem0-stack.lock
# 注释——如需从源码 tarball 侧核对,比对 lock 注释中的值)。
sha256sum /opt/cybergril-hel874/wheels/*.whl | tee /opt/cybergril-hel874/wheels/SHA256SUMS
# 4.2 同一发布进程离线并入(一个 venv 同时 import 产品与 mem0 —— 唯一被认可形态;
# uv pip install 是本机确实存在的子命令,--no-index 离线装)
uv pip install --python .venv311/bin/python --no-index \
--find-links /opt/cybergril-hel874/wheels -r deploy/mem0-stack.lock
.venv311/bin/python -c "import cybergril.api, mem0, qdrant_client, fastembed; print('co-import ok')"
```
`deploy/mem0-stack.lock` 是本卡新交的固定清单(§4.1 的 `-r` 输入)。在此之前
`CYBERGRIL_SEMANTIC` 保持 `stub`/`off`;**不得以 stub/FTS 降级包冒充用户的
真实完整记忆体验**(体验交付必须等本节全部完成 + 新增预算批准)。
**准确缺项与如实上报(HEL-885):** 本机当前没有这些 3.11 wheel,本卡零下载,
`wheels/` 目录与 SHA256SUMS 均不存在——发布卡执行 4.1 时才能产生。若某个固定
版本在 cp311/manylinux2014_x86_64 没有 wheel(`--only-binary :all:` 会直接报错
指出包名),按 lock 注释如实上报缺项,不得换版本号冒充固定安装、不得改用 sdist
混装。4.2 的 co-import 是最终判据:任何一个 import 失败即未完成,不得以 stub/
FTS 降级包冒充用户的真实完整记忆体验(体验交付必须等本节全部完成 + 新增预算
批准)。发布前可先跑 `python3 tools/check_release_offline_hel882.py` 的 R7
核验本节两条命令的工具面(pip download 存在、uv pip download 不存在)。
激活步骤(wheel 落好之后):
1. `cp config/semantic.example.yaml config/semantic.yaml`,总管填批准值:
`embed_cap`/`llm_cap`(B 部分累计)、`embed_daily`/`llm_daily`(C 部分日顶)、
`embed_reserve`;`ledger_path` 指向唯一权威 `embed_ledger.sqlite3` 续账
(`llm_ops` 同库新表),并保留 `authority` 冻结前缀块——指向新的空文件
绕过 498/500 冻结会被 `verify_ledger_authority` 直接拒绝(HEL-882)。
`embed_cap`/`llm_cap`(B 部分累计,换算算式见示例文件与执行表 §2——
500+增量,不是把增量直接填进累计上限)、`embed_daily`/`llm_daily`
(C 部分日顶)、`embed_reserve`;`ledger_path` 指向唯一权威
`embed_ledger.sqlite3` 续账(`llm_ops` 同库新表),并**填写** `authority`
冻结前缀块(HEL-885 起必填:缺/空/错一律在构造前拒绝
`ledger_authority_required`,指向新空文件绕过 498/500 冻结同样被拒)。
2. `CYBERGRIL_SEMANTIC=mem0` 重启。任何未批调用 fail-closed
(`semantic_*_budget_not_approved/exhausted/daily_exhausted`);SDK 内部
提取 LLM 同时过统一请求闸(`semantic` 类别 + 日 token 池,发送前预占)。
## 5. 停用与保留数据回退
- 停语义层:`CYBERGRIL_SEMANTIC=off`(或 stub)重启;`sem_batch/sem_covered`
按后端区分保留,重开续跑不重发已完成批次,stub 批次不阻塞真实重建。
- 停语义层:`CYBERGRIL_SEMANTIC=off`(或 stub)重启;`sem_batch/sem_covered/
sem_row_done` 按后端区分保留,重开续跑不重发已完成行(逐行锚点+逐行完成
记录,HEL-885),stub 批次不阻塞真实重建。
- 停主动联系/生活:`CYBERGRIL_R2_ENABLED=0`。
- 整实例回退:`systemctl stop cybergril-hel874` → 数据目录
`/opt/cybergril-hel874/data/` 原样保留(聊天/记忆/生活/情绪/关系全量 +
+26 -2
View File
@@ -38,6 +38,11 @@ HEL-857 系列冻结余额(真实请求 39/60、嵌入 498/500、HEL-835 闸
哪些已推理,整批保守落账。
- **Mem0 内部 LLM**:独立 `llm_ops` 累计账(B)+ `llm_daily` 日顶(C)+ 统一
请求闸(`semantic` 类别 + 日 token 池,发送前预占)。
- **未知预占不退款(HEL-885)**:`reserve:` 预占行不再按一小时超时清除——
已发送后崩溃未 settle 的消费事实必须保留;唯一删除路径是可证明未发送的
显式 `release()`(如 httpx.ConnectError 级未连接)。作业租约过期不是
「未发生」的证明。跨日结算/释放一律按预占凭据的 `hold.day` 记回原日,
不冲新一天、不产生负用量(`token_usage`/`request_usage` 同口径)。
## 1. A 部分 · 一次性团队整体检查(隔离空库,非用户档案)
@@ -71,12 +76,31 @@ HEL-857 系列冻结余额(真实请求 39/60、嵌入 498/500、HEL-835 闸
| 嵌入(提取批 + 实体 + 查询) | ≤ 180 条(`embed_cap`) |
| 对话/生活/判定等其余 | 并入 C 部分日账 |
**累计 cap / 保留量算式(HEL-885,写入 config 前照此换算,禁止把增量直接填进累计上限):**
```
权威账现状 :已用 498 条,原批上限 500,冻结不动余额 2 条
新批增量 :白栖知批准的 B 部分嵌入条数(示例 180)
embed_cap = 500 + 180 = 680 # 原批上限 + 新批增量(≠ 180)
embed_reserve= 2 # 原冻结余额继续保留,不得消耗
可用余量 = 680 − 498 − 2 = 180
authority = {prefix_rows: <权威账前N行>, prefix_sha256: <逐行摘要>,
min_used: 498} # HEL-885 起任一 cap>0 时必填,缺/空/错即拒绝构造
```
错误示例:对已用 498 的账把 `embed_cap` 填成 180(等于瞬间耗尽并掩盖历史用量);
以任何方式消耗旧冻结 2 条(`min_used: 498` 保证拒绝)。`authority` 块缺省或
整段注释时,mem0 模式在构造前 fail-closed(`ledger_authority_required`),
不再是可以跳过的可选项。
- 硬闸:`embed_cap`/`llm_cap`(累计)+ `embed_daily`/`llm_daily`(日顶)写入
config/semantic.yaml(默认全 0=拒绝);预占-落账事务制保证并发不越 cap;
`embed_reserve` 预留复测余量;`authority` 冻结前缀块拒绝新文件绕过 498/500。
- 碰上限即停:作业批回到 `queued`(reason=`semantic_*_budget_*`),**不耗重试
次数、不丢行**——总管调 cap 后同一批续跑,已入库的行不再重发(向量库级
幂等);检索返回空块、对话回落词面 FTS,主对话不中断;不自动翻页。
次数、不丢行**——总管调 cap 后同一批续跑,已完成的行不再重发(向量库逐行
锚点 + `sem_row_done` 逐行完成记录,HEL-885:每个向量只证明自己那一行,
空提取行同样有完成证明);检索返回空块、对话回落词面 FTS,主对话不中断;
不自动翻页。
- SDK 吞错已堵住:中途嵌入被预算挡住时 mem0 返回空列表,产品现在识别为预算
拒绝并重排队(不再误写 done/[] 造成永久漏整理)。
- 授权来源:**需白栖知明确批准**(提高嵌入上限或另批独立账;现有 498/500
+51
View File
@@ -0,0 +1,51 @@
{
"baseline_version": "1.11",
"read_commit": "59fb06304fb9f230837b43e431e2a1cc303e16c9",
"task": "HEL-885 返工(总管对 HEL-882 59fb063 独立复审五项缺口的一次归拢修正):逐行恢复与真实来源一致(每次 add 只带自己那一行的锚点,向量只证明自身行;空提取行落 sem_row_done 完成证明;预算失败/崩溃重启/取消租约/并发不漏行、不伪完成、不重发完成行)、跨日结算按预占日记回原日并维护累计账(不再冲出新日负用量)、未知已发送预占永不超时退款(reserve 行唯一删除路径是可证明未发送的 release,llm/embed 同路径)、正 cap 必须验证权威来源(authority 缺/空/错在构造前 fail-closed,示例配置必填并给出 498已用+冻结2+新批增量的累计 cap/保留量算式)、发布依赖补齐命令改为本机确实支持的 python3 -m pip download 并入 R7 离线探针;零真实调用、零真实嵌入、零非回环出站、不部署、不合并、不改视觉、不碰权威三账",
"requirement_ids": [
"CG-006",
"CG-008",
"CG-011",
"CG-013",
"CG-014",
"CG-015",
"CG-016",
"CG-021",
"AM-001",
"AM-002",
"AM-003",
"UX-004",
"UX-006",
"GOV-001",
"GOV-002",
"GOV-003",
"GOV-005",
"GOV-006",
"DEL-001"
],
"deliverable": "在统一候选母线 hel-870-assembly(基于 59fb063 追加,PR#11 只追加不强推)上完成五项修正:1) 逐行进度与来源证明——Mem0Backend/StubBackend add_group 改为逐行发送、每次 add 的 metadata 只带该行锚点(turn_ids/turn_roles/input_sha256 均单行),向量存在只证明自身行;新增 sem_row_done 表(r4.sql 增量迁移)在每行 add 完成瞬间于 worker 线程内落持久完成记录(合法空提取=零向量也有完成证明),tick 恢复时以 向量库锚点∪sem_row_done 为跳过集,批次 entry_ids 跨部分尝试合并;吞错检测改逐行口径。2) 跨日账——PersistentBudget.settle/release 一律写 hold.day(token_usage/request_usage 按预占日,budget_total 全局),不再向 state.day 冲账制造负用量;另修 budget_total 空表时 total_requests_used/total_tokens_used 的 TypeError 崩溃(coalesce 子查询),旧用例靠该崩溃“通过”的假绿改为如实入账。3) 未知预占——_atomic_reserve 删除 1 小时过期 DELETE(reserve 行不再按龄清除),未知消费保守计费、跨重启保留,唯一删除路径是可证明未发送的显式 release();embed/llm 共用路径同修。4) 权威来源必填——Mem0Backend 在 import mem0 之前:任一 cap>0 而 authority 缺/空/非 dict 即抛 ledger_authority_required(构造前拒绝,不发任何请求),错的 authority 仍被 verify_ledger_authority 前缀校验拒绝;config/semantic.example.yaml authority 段改为必填示例并附累计算式(embed_cap=500+增量、embed_reserve=2、min_used=498,禁把 180 直接填成累计 cap、禁动旧冻结 2 条);docs/budget-plan-hel874.md §2 补同套算式与 HEL-885 记账口径。5) 发布命令——deploy/hel874-release.md §4 改为 python3 -m pip download(--only-binary :all: --platform manylinux2014_x86_64 --python-version 3.11 --implementation cp --abi cp311)+ sha256 留证 + uv pip install --no-index --find-links 离线并入 + 同进程 co-import 判据,如实列明本机无 3.11 wheel、本卡零下载、无包则如实上报不换版本;tools/check_release_offline_hel882.py 新增 R7 双探针(pip download 存在 rc=0、已删除的 uv pip download 保持不存在)。验证:3.11.16 venv+requirements.lock 离线 281 项 pytest 全过(新增 tests/test_rework_hel885.py 9 例:部分批次只认已发行/空提取不重发/进程重启只补缺行/并发 tick 不双发/跨日结算与释放按预占日/未知预占 llm+embed 不退款/缺空错 authority fail-closed;test_resume_never_loses_or_duplicates_rows 重写为真实产品路径,不再手工植入单行锚点),ruff 0 错(src/tests/tools),web tsc+42 vitest+build(npm ci --offline),固定 mem0 2.2.1 安装确定性替身:verify_mem0_real_hel874.py 12/12、新增 verify_semantic_recovery_hel885.py 7/7(S0 缺 authority 拒绝/S1 逐行部分/S2 重启只补缺行以 HTTP 计数证明/S3 租约回收+并发不双发/S4 未知预占不退款),总管 review_hel882_{partial,extra}.py 两脚本原样复跑全过(证据 docs/rework/hel885/),发布离线检查 R1–R7 全过;零真实调用、零真实嵌入、零非回环出站,权威三账与冻结余额未写,未部署未合并未交体验地址,不改视觉。",
"acceptance": [
"总管 partial 复现修复:首 tick 后 completed_message_ids=[2](仅伴侣行,向量逐行锚点),补 cap 后 done、entry_ids 2 条、回环 HTTP=2、用户原话确实发出且每行恰一次(manager-review-partial-rerun.json 原样脚本复跑)",
"总管 extra 复现修复:跨日结算次日 used=0、估算 150 被拒、原日记实际 10;未知预占 second_allowed=false、used 保持 1(manager-review-extra-rerun.json 原样脚本复跑)",
"逐行恢复真实产品路径:空提取行有 sem_row_done 完成证明且不再重发;进程重启后只补缺行;并发 tick 不双发(tests/test_rework_hel885.py + recovery-real-sdk.json S1–S3)",
"未知预占保守计费:llm_ops 与 embed_ops 同路径,一小时前的未结算预占跨重启仍占额度,唯一删除路径是显式可证明未发送的 release(S4a/S4b + 对应用例)",
"authority 必填:缺/空/错 authority 在构造前拒绝(ledger_authority_required / 前缀不匹配),示例配置必填并附 500+增量、reserve=2、min_used=498 算式,明确禁把 180 填成累计 cap、禁动旧冻结 2 条(tests + recovery-real-sdk.json S0 + semantic.example.yaml + budget-plan-hel874.md)",
"发布命令真实可用:§4 改为 python3 -m pip download(本机实测 rc=0),R7 双探针入离线检查;本机无 3.11 wheel、零下载、缺包如实上报的口径不变(release-offline-hel885.json)",
"工程检查:281 pytest、ruff 0(src/tests/tools)、web typecheck/42 vitest/build、双目标需求检查 4/4(含本 handoff);固定 SDK 合同 12/12 复跑通过"
],
"deferred": [
"发布与体验地址:由总管独立复审本 head 后交发布卡按 deploy/hel874-release.md 执行(8360 隔离实例);本卡未部署未合并 PR#11 未交体验地址",
"真实语义激活(CYBERGRIL_SEMANTIC=mem0):需 B 部分预算批准 + 3.11 mem0 栈 wheel 落位(按 §4 新命令执行并留 sha256 证);未激活前 stub/FTS 不冒充真实记忆体验",
"A/B/C 执行表数值仍为待批建议:批准前所有 cap 保持 0=拒绝;authority 前缀实际值由总管按权威账现状填写;新增预算由总管向白栖知一次性申请",
"R5 语音/R6 天气、素材、备份/R7 有条件观察承接不变;视觉仍交原作者,本卡未改视觉",
"真实体验效果待体验地址交付后由白栖知判断"
],
"requirement_changes": [],
"verification": [
"分支 hel-870-assembly 追加提交(最终 head 见交付评论),PR#11 只追加未强推未合并;视觉总监分支未覆盖",
"python 3.11.16 venv + requirements.lock 离线:281 项测试全过(新增 tests/test_rework_hel885.py 9 例,重写 test_resume_never_loses_or_duplicates_rows);ruff 0 错(src/tests/tools);web typecheck/vitest 42/build 通过(npm ci --offline)",
"固定 mem0 2.2.1 安装(hel857-venv 3.10.12)确定性替身实跑:tools/verify_mem0_real_hel874.py 12/12;新增 tools/verify_semantic_recovery_hel885.py 7/7(零真实嵌入、零非回环);总管 review_hel882_{partial,extra}.py 原样复跑证据 docs/rework/hel885/manager-review-{partial,extra}-rerun.json",
"tools/check_release_offline_hel882.py(echo 替身、随机回环端口、临时目录)R1–R7 全过;mem0 同进程导入缺项(3.11 无 wheel)如实记录,解决路径 deploy/hel874-release.md §4 新命令,本卡零下载",
"python3 scripts/check_requirements.py --base-ref origin/main [--handoff docs/requirements/handoff-HEL-885.json] 与 --base-ref origin/agent/agent/de8874a3803b [--handoff …] 四项通过"
]
}
+43
View File
@@ -0,0 +1,43 @@
# HEL-885 · 总管独立复审五项修正证据
读取基线:`59fb06304fb9f230837b43e431e2a1cc303e16c9`(HEL-882 最终 head,
总管独立复审「暂不通过发布审核」)。本目录证据均在修正后的代码上实际执行,
边界与 HEL-882 一致:确定性 512 维向量替身(TextEmbedding 构造前安装)、
socket 拒绝非回环、127.0.0.1 假网关、临时账/库,零真实 LLM、零真实嵌入、
零下载、零权威三账写入。
## 修正对照(复现 → 修复后实测)
| # | 总管复现(59fb063) | 修复后实测 | 证据 |
| --- | --- | --- | --- |
| 1 | 整批 send_anchor 复制给每次逐行 add;首向量即宣称两行完成;补 cap 后 done/[]、HTTP 仍 1、用户原话从未发出 | 每向量逐行锚点;首 tick 后 completed=[2];补 cap 后 done 且 entry_ids 2 条、HTTP=2、用户原话确实发出(每行恰一次) | manager-review-partial-rerun.json(脚本原样复跑) |
| 2 | settle 用 state.day:次日 used=-80,估算 150 放行 | 结算记回预占日:次日 used=0,150 被拒,原日记实际 10 | manager-review-extra-rerun.json(脚本原样复跑) |
| 3 | reserve:* 一小时 DELETE:未知预占被删,第二次 reserve 又放行 | 未知预占保留:second_allowed=false,used 保持 1(llm/embed 同路径、跨重启) | manager-review-extra-rerun.json + recovery-real-sdk.json S4a/S4b |
| 4 | 缺 authority 时跳过校验:新空账+正 cap 成功构造并发送 | 缺/空/错 authority 一律构造前拒绝 ledger_authority_required;示例配置 authority 必填+算式 | recovery-real-sdk.json S0 + tests/test_rework_hel885.py |
| 5 | `uv pip download` 本机不存在(rc=2) | 文档改用 `python3 -m pip download`(rc=0 实测);R7 双探针入离线检查 | release-offline-hel885.json R7 |
## 文件
- `review_hel882_partial.py` / `review_hel882_extra.py`:总管原样脚本(附件同版,
自 hel-882-bcbb63d27399 工作目录复制),对本 checkout 复跑。
- `manager-review-partial-rerun.json` / `manager-review-extra-rerun.json`:
原样脚本复跑结果(head 字段仍为脚本内写定的 59fb063 字符串,实际执行于本
修正后的 checkout;JSON 内容即修复后行为)。
- `recovery-real-sdk.json`:tools/verify_semantic_recovery_hel885.py(新增)——
固定 mem0ai 2.2.1 安装上 S0 缺 authority 拒绝 / S1 逐行部分完成 / S2 进程重启
只补缺行(HTTP 计数证明,不依赖文本出现次数:mem0 提取提示会合法携带旧行
滚动历史)/ S3 租约回收与并发 tick 不双发 / S4a/S4b 未知预占不退款。
- `real-sdk-contract.json`:tools/verify_mem0_real_hel874.py 复跑 12/12
(make_ledger/config_for 现带合成 authority 前缀,产品默认未被放宽)。
- `release-offline-hel885.json`:tools/check_release_offline_hel882.py 全过,
新增 R7(pip download 存在 rc=0;已删除的 uv pip download 保持不存在)。
## 回归
- 3.11.16 venv + requirements.lock 离线:281 项 pytest 全过(新增
tests/test_rework_hel885.py 9 例;test_resume_never_loses_or_duplicates_rows
重写为真实产品路径,不再手工植入单行锚点);ruff 0 错(src/tests/tools);
web tsc + 42 vitest + build(npm ci --offline)。
- 发现并修复一处被旧测试掩盖的崩溃:budget_total 空表时
total_tokens_used() 抛 TypeError(旧用例靠该崩溃“通过”)——现
coalesce+空行处理,用例改为如实入账一次性总量。
@@ -0,0 +1,33 @@
{
"head": "59fb06304fb9f230837b43e431e2a1cc303e16c9",
"real_llm_requests": 0,
"real_embeddings": 0,
"midnight_settlement": {
"daily_cap": 100,
"old_hold": 90,
"actual": 10,
"new_day_used_before_next_request": 0,
"request_estimate": 150,
"over_cap_request_allowed": false,
"rows": [
{
"day": "2026-10-26",
"scene": "dialogue_main",
"used": 10
}
]
},
"aged_unknown_reservation": {
"cap": 1,
"used_before": 1,
"second_allowed": false,
"first_hold_id": 1,
"second_hold_id": null,
"used_after": 1
},
"release_documented_download_command": {
"command": "uv pip download --help",
"returncode": 2,
"stderr": "error: unrecognized subcommand 'download'\n\nUsage: uv pip [OPTIONS] <COMMAND>\n\nFor more information, try '--help'."
}
}
@@ -0,0 +1,48 @@
{
"head": "59fb06304fb9f230837b43e431e2a1cc303e16c9",
"real_llm_requests": 0,
"real_embeddings": 0,
"boundary": "deterministic 512 vectors, non-loopback forbidden",
"missing_authority_constructed_with_positive_caps": false,
"public_batch_enqueued": 1,
"batch_after_first_tick": {
"state": "queued",
"attempts": 0,
"reason": "LLM extraction failed: semantic_llm_budget_exhausted",
"entry_ids": null
},
"loopback_requests_before_resume": 1,
"stored_points_after_first": [
{
"src_group": "chat_public:mem0:synthetic:1bc8667410",
"turn_ids": [
"synthetic"
],
"turn_roles": [
[
"synthetic",
"companion",
2
]
],
"input_sha256": "840dde45eec07416f21c56cefb0f93f4e71af3edc3100c332f84bb1266637862",
"user_id": "cybergril",
"data": "合成提取记忆HEL874-1",
"text_lemmatized": "合成提取记忆HEL874-1",
"hash": "69195446402294f6e77f8ee993ce3456",
"created_at": "2026-10-26T15:56:46.108106+00:00",
"updated_at": "2026-10-26T15:56:46.108106+00:00"
}
],
"claimed_completed_message_ids": [
2
],
"batch_after_resume": {
"state": "done",
"attempts": 1,
"reason": null,
"entry_ids": "[\"2bc5a040-cd80-47a6-b865-c3b3edb7c711\", \"fe98487a-e3f4-4633-abda-2680544a9aaf\"]"
},
"loopback_requests_after_resume": 2,
"unsent_user_row_actually_sent": true
}
+172
View File
@@ -0,0 +1,172 @@
{
"started": "2026-10-26T15:58:41Z",
"embedding_boundary": "deterministic 512-dimensional vectors installed before TextEmbedding construction; no model loaded; zero real embeddings",
"external_network": "socket.connect rejects all non-loopback hosts",
"install": {
"python": "3.10.12",
"mem0ai": "2.2.1",
"qdrant-client": "1.20.0",
"fastembed": "0.9.1",
"mem0_path": "/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir/hel857-venv/lib/python3.10/site-packages/mem0",
"module_sha256": {
"memory/main.py": "f329ca4a17af1fbefb32ac10d4cc7ae7ddbdb7435a87eb74c30915cff98b8b7c",
"memory/storage.py": "5f5ecdadd2b260fd14f36a0f338336eb760275465dd1056a99a47e1ef07702bd",
"embeddings/fastembed.py": "3ec2ae87df5d0bebedb8b5471366c3412975847458499f66097f7b8c9ac8c9b8",
"llms/openai.py": "37e6d3c22b55df0731740f4e0868b95bd480ce44363227fe53461db328c5f67e",
"vector_stores/qdrant.py": "da570579d5cad69e6f123fdfe32b725235a25604e70047666bec763ba6e2bf43"
},
"source_tarball_sha256": "9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23"
},
"adapter_has_llm_guard": true,
"checks": [
{
"name": "C1_construct",
"ok": true,
"detail": "client_path=sdk-vector-store-client"
},
{
"name": "C2_add_anchor_payload",
"ok": true,
"detail": "created=True payload_keys=['created_at', 'data', 'hash', 'input_sha256', 'src_group', 'text_lemmatized', 'turn_ids', 'turn_roles']"
},
{
"name": "C2_add_user_scope",
"ok": true,
"detail": "user_id present in stored payload"
},
{
"name": "C2_add_ledger_shape",
"ok": true,
"detail": "embed_items=3 llm_calls=1"
},
{
"name": "C3_extraction_input_set",
"ok": true,
"detail": "old_memory=True new_row=True history_carry=True"
},
{
"name": "C3b_unshared_canary_never_outbound",
"ok": true,
"detail": "queued_public_batches=2 leaked=False anchor_taint=False"
},
{
"name": "C4_search_filters",
"ok": true,
"detail": "hits=3"
},
{
"name": "C4_search_single_debit",
"ok": true,
"detail": "embed_delta=1 (pre-fix=2)"
},
{
"name": "C6_concurrency_cap",
"ok": true,
"detail": "outcomes=['ok', 'refused'] used=3 cap=3"
},
{
"name": "C7_reopen_after_close",
"ok": true,
"detail": "hits_after_reopen=3"
},
{
"name": "C5_embed_zero_refuses",
"ok": true,
"detail": "err=semantic_embed_budget_not_approved llm_sent=0"
},
{
"name": "C5_llm_zero_refuses",
"ok": true,
"detail": "err=semantic_llm_budget_not_approved llm_sent=0"
}
],
"embed_rows_after_first_add": [
{
"kind": "authority_prefix",
"items": 0,
"note": "authority:synthetic-verify-prefix"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
},
{
"kind": "dense",
"items": 1,
"note": "product:batch:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:add"
}
],
"final_embed_rows": [
{
"kind": "authority_prefix",
"items": 0,
"note": "authority:synthetic-verify-prefix"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
},
{
"kind": "dense",
"items": 1,
"note": "product:batch:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
},
{
"kind": "dense",
"items": 1,
"note": "product:batch:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
},
{
"kind": "dense",
"items": 1,
"note": "product:batch:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
}
],
"final_llm_used": 3,
"finished": "2026-10-26T15:58:42Z",
"gateway_requests_total": 3,
"failures": [],
"temp_dir": "/tmp/multica-task-4169170952/hel874-real-sdk-zbdu5fkf"
}
+45
View File
@@ -0,0 +1,45 @@
{
"head": "HEL-885 rework",
"real_llm_requests": 0,
"real_embeddings": 0,
"boundary": "deterministic 512 vectors, non-loopback forbidden",
"checks": [
{
"name": "S0_positive_caps_without_authority_fail_closed",
"ok": true,
"detail": "missing=ledger_authority_required empty=ledger_authority_required"
},
{
"name": "S1_p1_first_tick_partial",
"ok": true,
"detail": "state=queued reason=LLM extraction failed: semantic_llm_budget_exhausted http=1 completed=[2] points=1 per_row_anchor=True"
},
{
"name": "S2_p1_restart_sends_only_missing_row",
"ok": true,
"detail": "state=done http=1 user_sent=True completed=[1, 2] entry_ids=[\"0419c0c8-a6b1-4a24-864b-295658e23b19\", \"3d1de06a-bc8c-4f4b-9914-049ca13edb5e\"]"
},
{
"name": "S3a_lease_expired_batch_recovers",
"ok": true,
"detail": "recovered={'state': 'failed', 'reason': 'lease_expired'} state_after=done http=2"
},
{
"name": "S3b_concurrent_ticks_single_send",
"ok": true,
"detail": "http=2 states=['done']"
},
{
"name": "S4a_aged_unknown_llm_reservation_kept",
"ok": true,
"detail": "second_allowed=False used_after_restart=1"
},
{
"name": "S4b_aged_unknown_embed_reservation_kept",
"ok": true,
"detail": "second_allowed=False used=1 after_release=0"
}
],
"gateway_requests_total": 6,
"failures": []
}
@@ -0,0 +1,86 @@
{
"started": "2026-10-26T15:58:29Z",
"venv_python": "/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-874-db7635e6b65e/workdir/CyberGril/.venv311/bin/python",
"venv_python_version": "3.11.16",
"checks": [
{
"name": "R1_checkout_fresh_dir",
"ok": true,
"detail": "cloned tracked source into /tmp/multica-task-4169170952/hel882-release-qd9sq7nv/cybergril-hel874/current"
},
{
"name": "R2_service_imports_product",
"ok": true,
"detail": ""
},
{
"name": "R6_mem0_coimport_status_recorded",
"ok": true,
"detail": "import mem0 → ModuleNotFoundError: No module named 'mem0' (resolution path in deploy/hel874-release.md §4; no download in this check)"
},
{
"name": "R7_pip_download_command_exists",
"ok": true,
"detail": "/usr/bin/python3 -m pip download --help → rc=0"
},
{
"name": "R7_removed_uv_pip_download_still_absent",
"ok": true,
"detail": "uv pip download --help → rc=2 (kept absent on purpose; do not re-document it)"
},
{
"name": "R2_service_serves_health",
"ok": true,
"detail": "version=R4-assembly semantic=stub"
},
{
"name": "R3_create_status_recorded",
"ok": true,
"detail": "create status=201(409=前次运行的档案仍在,也是保留数据的表现)"
},
{
"name": "R3_dialogue_refused_honestly",
"ok": true,
"detail": "http=200 events=b'data: {\"t\": \"chat.error\", \"epoch\": 1, \"turn_id\": \"00000000-0000-4000-8000-000000000001\", \"phi\": \"\\xe5\\xa5\\xb9\\xe8\\xbf\\x99\\xe4\\xbc\\x9a\\xe5\\x84\\xbf\\xe6\\xb2\\xa1\\xe6\\x8e\\xa5\\xe4\\xb8\\x8a\\xe8\\xaf'"
},
{
"name": "R4_service_restarts_on_same_data",
"ok": true,
"detail": "epoch=2"
},
{
"name": "R5_epoch_bumped_on_reopen",
"ok": true,
"detail": "1 → 2"
},
{
"name": "R4_companion_created_offline",
"ok": true,
"detail": "status=409"
},
{
"name": "R4_dialogue_stream_http",
"ok": true,
"detail": "status=200 events=b'data: {\"t\": \"chat.delta\", \"epoch\": 2, \"turn_id\": \"00000000-0000-4000-8000-000000000002\", \"delta\": \"\\xe6\\x89\\xb9\"}\\n\\ndata: {\"t\": \"c'"
},
{
"name": "R4_messages_roundtrip",
"ok": true,
"detail": "roles=['user', 'user', 'companion']"
},
{
"name": "R5_data_dir_preserved",
"ok": true,
"detail": "['companion.db']"
},
{
"name": "R5_companion_row_survives_restart",
"ok": true,
"detail": "companion rows=1"
}
],
"mem0_import_in_release_venv": "ModuleNotFoundError: No module named 'mem0'",
"removed_uv_pip_download_rc": 2,
"failures": [],
"finished": "2026-10-26T15:58:34Z"
}
+60
View File
@@ -0,0 +1,60 @@
"""Independent regressions, temporary ledgers, no provider or remote access."""
import json
import sqlite3
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT / 'CyberGril/src'))
from cybergril.life.model import PersistentBudget
from cybergril.memory.store import Store
from cybergril.memory.semantic import LlmLedgerGuard
from cybergril.think.budget import BudgetConfig, BudgetGuard
result = {'head': '59fb06304fb9f230837b43e431e2a1cc303e16c9',
'real_llm_requests': 0, 'real_embeddings': 0}
with tempfile.TemporaryDirectory(dir=ROOT) as td:
tmp = Path(td)
store = Store(tmp / 'midnight.db')
guard = PersistentBudget(store, BudgetGuard(BudgetConfig(
daily_tokens=100, daily_requests={'dialogue': 10})))
guard._roll = lambda: None
guard.state.day = '2026-10-26'
hold = guard.acquire('dialogue_main', tokens=90)
guard.state.day = '2026-10-27'
guard.settle(hold, actual_tokens=10)
used = guard.used
try:
guard.acquire('dialogue_main', tokens=150)
allowed = True
except RuntimeError:
allowed = False
result['midnight_settlement'] = {
'daily_cap': 100, 'old_hold': 90, 'actual': 10,
'new_day_used_before_next_request': used,
'request_estimate': 150, 'over_cap_request_allowed': allowed,
'rows': [dict(r) for r in store.query('SELECT * FROM token_usage')]}
store.close()
ledger = tmp / 'crash.db'
llm = LlmLedgerGuard(ledger, cap=1)
pending = llm.reserve(1, 'sent_but_crashed_before_settlement')
with sqlite3.connect(ledger) as conn:
conn.execute("UPDATE llm_ops SET ts='2000-01-01T00:00:00Z' WHERE id=?", (pending,))
before = llm.used()
try:
second = llm.reserve(1, 'second_send_after_restart')
allowed = True
except RuntimeError:
second, allowed = None, False
result['aged_unknown_reservation'] = {
'cap': 1, 'used_before': before, 'second_allowed': allowed,
'first_hold_id': pending, 'second_hold_id': second,
'used_after': llm.used()}
probe = subprocess.run(['uv','pip','download','--help'], capture_output=True, text=True)
result['release_documented_download_command'] = {
'command': 'uv pip download --help', 'returncode': probe.returncode,
'stderr': probe.stderr.strip()}
(ROOT / 'HEL-882-extra-independent.json').write_text(json.dumps(result, ensure_ascii=False, indent=2))
print(json.dumps(result, ensure_ascii=False, indent=2))
@@ -0,0 +1,68 @@
"""Actual pinned SDK; deterministic vectors and loopback HTTP only."""
import asyncio
import importlib.util
import json
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent
REPO = ROOT / 'CyberGril'
sys.path.insert(0, str(REPO / 'src'))
spec = importlib.util.spec_from_file_location('contract', REPO / 'tools/verify_mem0_real_hel874.py')
contract = importlib.util.module_from_spec(spec)
spec.loader.exec_module(contract)
contract.install_deterministic_boundary()
from cybergril.memory.semantic import Mem0Backend, SemanticMemory
from cybergril.memory.store import Store
async def main():
result = {'head': '59fb06304fb9f230837b43e431e2a1cc303e16c9',
'real_llm_requests': 0, 'real_embeddings': 0,
'boundary': 'deterministic 512 vectors, non-loopback forbidden'}
with tempfile.TemporaryDirectory(dir=ROOT) as td:
tmp = Path(td)
server = contract.start_gateway()
backend = None
try:
ledger = tmp / 'ledger.db'
contract.make_ledger(ledger, cap=100, llm_cap=1)
cfg = contract.config_for(tmp, ledger, port=server.server_address[1], cap=100, llm_cap=1)
backend = Mem0Backend(cfg)
result['missing_authority_constructed_with_positive_caps'] = 'authority' not in cfg
store = Store(tmp / 'product.db')
with store.conn:
store.execute("INSERT INTO chat_message(ts,role,text,turn_id,status) VALUES"
"(1,'user','合成用户原话甲','synthetic','sent')")
inserted = store.execute("INSERT INTO chat_message(ts,role,text,turn_id,status) VALUES"
"(2,'companion','合成已公开伴侣原话乙','synthetic','sent')")
store.execute("INSERT INTO sharing_attempt(message_id,turn_id,epoch,token,text_hash,expires_at,"
"ack_at,candidates,state) VALUES(?,'synthetic',1,'synthetic','synthetic',999999,3,'[]','judged')",
(inserted.lastrowid,))
live = SemanticMemory(store, backend)
result['public_batch_enqueued'] = live.enqueue_uncovered()
await live.tick()
job = dict(store.query_one('SELECT * FROM sem_batch'))
result['batch_after_first_tick'] = {k: job[k] for k in ('state','attempts','reason','entry_ids')}
result['loopback_requests_before_resume'] = len(contract.FakeGateway.requests)
points, _ = backend._client.scroll(collection_name=backend.collection, with_payload=True, limit=100)
result['stored_points_after_first'] = [dict(p.payload) for p in points]
result['claimed_completed_message_ids'] = sorted(backend.completed_message_ids(job['batch_key']))
backend.llm_guard.cap = 5
await live.tick()
result['batch_after_resume'] = dict(store.query_one('SELECT state,attempts,reason,entry_ids FROM sem_batch'))
result['loopback_requests_after_resume'] = len(contract.FakeGateway.requests)
result['unsent_user_row_actually_sent'] = any(
'合成用户原话甲' in json.dumps(json.loads(r['body']), ensure_ascii=False)
for r in contract.FakeGateway.requests)
store.close()
finally:
if backend:
backend._memory.llm.client.close()
await backend.aclose()
server.shutdown()
server.server_close()
(ROOT / 'HEL-882-partial-independent.json').write_text(json.dumps(result, ensure_ascii=False, indent=2))
print(json.dumps(result, ensure_ascii=False, indent=2))
asyncio.run(main())
+26 -11
View File
@@ -50,14 +50,16 @@ class PersistentBudget(BudgetGuard):
)["n"]
def total_requests_used(self):
return self.store.query_one(
"SELECT coalesce(value,0) v FROM budget_total WHERE key='total_requests'"
)["v"]
row = self.store.query_one(
"SELECT coalesce((SELECT value FROM budget_total WHERE key='total_requests'),0) v"
)
return row["v"] if row else 0
def total_tokens_used(self):
return self.store.query_one(
"SELECT coalesce(value,0) v FROM budget_total WHERE key='total_tokens'"
)["v"]
row = self.store.query_one(
"SELECT coalesce((SELECT value FROM budget_total WHERE key='total_tokens'),0) v"
)
return row["v"] if row else 0
def reserve(self, tokens, scene):
"""Legacy background-scene token reservation (95% stop), kept for compatibility."""
@@ -126,7 +128,14 @@ class PersistentBudget(BudgetGuard):
reserved_tokens=tokens, requests=requests, day=day)
def settle(self, hold, *, actual_tokens=None):
"""Real usage adjusts the reserved tokens; unknown usage keeps the reserve."""
"""Real usage adjusts the reserved tokens; unknown usage keeps the reserve.
HEL-885: the correction lands on the day the hold was RESERVED on
(``hold.day``) — a request crossing midnight adjusts the old day's row
to its actual spend. Writing to ``self.state.day`` (the HEL-882 bug)
manufactured negative usage on the new day and re-opened headroom the
old day had already consumed.
"""
if hold is None:
return
self._roll()
@@ -137,7 +146,7 @@ class PersistentBudget(BudgetGuard):
self.store.execute(
"INSERT INTO token_usage VALUES(?,?,?) ON CONFLICT(day,scene) DO UPDATE "
"SET used=used+excluded.used",
(self.state.day, hold.scene, delta),
(hold.day, hold.scene, delta),
)
self.store.execute(
"INSERT INTO budget_total VALUES('total_tokens',?) ON CONFLICT(key) DO UPDATE "
@@ -145,7 +154,13 @@ class PersistentBudget(BudgetGuard):
)
def release(self, hold):
"""Refund only provably-unsent reservations (no provider, nothing left)."""
"""Refund only provably-unsent reservations (no provider, nothing left).
HEL-885: the refund returns to ``hold.day`` — the day that actually
paid the reservation — for the daily tables too; the one-shot
``budget_total`` counters are cross-day and refund in place. Refunding
into ``self.state.day`` inflated the new day with negative usage.
"""
if hold is None:
return
self._roll()
@@ -153,12 +168,12 @@ class PersistentBudget(BudgetGuard):
self.store.execute(
"INSERT INTO token_usage VALUES(?,?,?) ON CONFLICT(day,scene) DO UPDATE "
"SET used=used+excluded.used",
(self.state.day, hold.scene, -hold.reserved_tokens),
(hold.day, hold.scene, -hold.reserved_tokens),
)
self.store.execute(
"INSERT INTO request_usage VALUES(?,?,?) ON CONFLICT(day,category) DO UPDATE "
"SET requests=requests+excluded.requests",
(self.state.day, hold.category, -hold.requests),
(hold.day, hold.category, -hold.requests),
)
self.store.execute(
"INSERT INTO budget_total VALUES('total_requests',?) ON CONFLICT(key) DO UPDATE "
+11
View File
@@ -22,6 +22,17 @@ CREATE TABLE IF NOT EXISTS sem_covered (
message_id INTEGER PRIMARY KEY REFERENCES chat_message(id),
batch_id INTEGER NOT NULL REFERENCES sem_batch(id)
);
-- HEL-885 per-row completion: durable proof one row's add fully finished —
-- including a legitimate EMPTY extraction that stored no vector. Resume skips
-- rows proven done here or by vector-store anchors; finished rows are never
-- re-sent after a crash, restart or budget-refusal cycle.
CREATE TABLE IF NOT EXISTS sem_row_done (
batch_id INTEGER NOT NULL REFERENCES sem_batch(id),
message_id INTEGER NOT NULL,
entry_ids TEXT NOT NULL DEFAULT '[]', -- JSON list of backend entry ids this row created
done_at INTEGER NOT NULL,
PRIMARY KEY(batch_id, message_id)
);
-- Life activity continuity: start / continue / interrupt / resume / finish.
CREATE TABLE IF NOT EXISTS life_activity (
+147 -46
View File
@@ -41,6 +41,20 @@ Product port of the proven HEL-857/861–869 isolation results (plan-next-HEL-86
refusal re-queues the batch without burning an attempt; once budget returns,
the job resumes and skips rows whose entries already live in the vector store
(store-level idempotency: no lost rows, no duplicated sends, restart-safe).
- Per-row progress (HEL-885): every ``Memory.add`` carries an anchor for exactly
the ONE row it sends — a stored vector vouches for its own row and nothing
else (the HEL-882 whole-batch anchor let the first vector mark unsent rows
complete; the manager's real-SDK reproduction showed the user row never
leaving the process). A row whose add fully finished — including a
legitimate EMPTY extraction that stored no vector — is recorded in
``sem_row_done``; resume skips rows proven done by either the vector-store
anchors or that durable row record, so no row is lost, none is falsely
complete, and finished work is never re-sent.
- Unknown reservations are never refunded (HEL-885): a reservation whose
outcome is unknown (worker crashed after the wire, never settled) stays
counted forever — a lease expiring is not proof the call never happened.
Only ``release()`` with provably-unsent evidence (nothing hit the wire)
ever removes a hold.
- The stub backend is an orchestration double for engineering verification and
isolated tests only; it is not semantic and never claimed as such.
"""
@@ -144,17 +158,32 @@ class StubBackend:
self._entries: dict[str, dict] = {}
self._seq = 0
async def add_group(self, rows: list[dict], anchor: dict, batch_key: str) -> list[dict]:
async def add_group(self, rows: list[dict], anchor: dict, batch_key: str, *,
skip: set[int] | frozenset[int] = frozenset(),
on_row_done=None) -> list[dict]:
"""Per-row mirror of the mem0 contract: one entry per row, each with
that row's own anchor (HEL-885) — orchestration progress parity."""
def _add() -> list[dict]:
done = self.completed_message_ids(batch_key) | set(skip)
send = [r for r in rows if r["message_id"] not in done]
created = []
text = "\n".join(r["text"] for r in rows)
self._seq += 1
entry_id = f"stub-{self._seq}"
self._entries[entry_id] = {
"id": entry_id, "data": text,
"payload": {"data": text, "src_group": batch_key, **anchor},
}
created.append({"id": entry_id, "data": text})
for row in send:
row_anchor = {
"turn_ids": [row["turn_id"]],
"turn_roles": [[row["turn_id"], row["role"], row["message_id"]]],
"input_sha256": anchor_fingerprint([row]),
}
self._seq += 1
entry_id = f"stub-{self._seq}"
self._entries[entry_id] = {
"id": entry_id, "data": row["text"],
"payload": {"data": row["text"], "src_group": batch_key, **row_anchor},
}
entry = {"id": entry_id, "data": row["text"]}
created.append(entry)
if on_row_done is not None:
on_row_done(row, [entry])
return created
return await asyncio.to_thread(_add)
@@ -199,7 +228,9 @@ class EmbedLedgerGuard:
HEL-874: debits go through ``reserve``/``settle``/``release`` so the
check-and-hold is one serialized SQLite transaction — concurrent workers can
never overspend the cap. Stale holds (crashed workers) are purged by age.
never overspend the cap. Stale holds are NEVER purged (HEL-885): a crashed
worker's reservation has an unknown outcome and stays counted; only a
provably-unsent ``release()`` removes it.
HEL-882: ``daily_cap`` adds the execution-table C per-day top on top of the
lifetime cap (both enforced, tightest wins; the ts column is UTC ISO so the
@@ -290,8 +321,15 @@ def _pid() -> int:
return os.getpid()
def _reserve_cutoff() -> str:
"""ISO stamp older than any live reservation may be (crashed workers)."""
def _reserve_cutoff() -> str: # noqa: DEE001 — kept for audit tooling reference
"""Deprecated (HEL-885): aged ``reserve:`` rows are NEVER auto-deleted.
A hold whose worker died is an UNKNOWN outcome — the request may well be
sitting answered on the wire (the exact "sent then crashed before settle"
case). The manager's independent reproduction showed the old 1-hour purge
re-opening cumulative caps. ``_atomic_reserve`` no longer calls this; it
stays only so historical audit scripts importing it keep working.
"""
import time as _time
return _time.strftime("%Y-%m-%dT%H:%M:%SZ", _time.gmtime(_time.time() - 3600))
@@ -306,6 +344,11 @@ def _atomic_reserve(conn, *, table: str, cap: int, keep: int, items: int, note:
same serialized transaction — lifetime cap and daily top both apply, the
tightest one refuses. Reservation rows carry today's UTC timestamp so the
pending holds themselves count toward the day they were taken on.
HEL-885: aged ``reserve:`` rows are never purged here. A reservation is
removed ONLY by ``release()`` with provably-unsent evidence — a crashed
worker's hold has an unknown outcome and stays counted (conservative:
unknown consumption is billed, never refunded as "never happened").
"""
import time as _time
@@ -313,10 +356,6 @@ def _atomic_reserve(conn, *, table: str, cap: int, keep: int, items: int, note:
now = _time.strftime("%Y-%m-%dT%H:%M:%SZ", _time.gmtime())
conn.execute("BEGIN IMMEDIATE")
try:
conn.execute(
f"DELETE FROM {table} WHERE note LIKE ? AND ts < ?",
(EmbedLedgerGuard.RESERVE_NOTE + "%", _reserve_cutoff()),
)
if counted_kinds:
marks = ",".join("?" * len(counted_kinds))
row = conn.execute(
@@ -552,22 +591,28 @@ class Mem0Backend:
# Must precede the mem0 import; same ordering guarantee as the spike.
os.environ.setdefault("MEM0_TELEMETRY", "False")
try:
from mem0 import Memory # noqa: PLC0415 — fixed install, lazy by design
except ImportError as exc: # fail closed, never a silent fallback
raise RuntimeError("mem0_not_installed") from exc
self.ledger = ledger or EmbedLedgerGuard(
config.get("ledger_path", ""), config.get("embed_cap", 0),
daily_cap=config.get("embed_daily"))
self.llm_guard = llm_guard or LlmLedgerGuard(
config.get("ledger_path", ""), config.get("llm_cap", 0),
daily_cap=config.get("llm_daily"))
# HEL-882: a fresh file must never re-open the frozen balance — with any
# approved cap the configured ledger has to still be the authoritative
# account carrying the frozen prefix.
# HEL-885: the authority block is REQUIRED, never optional. The
# HEL-882 code skipped verification entirely when ``authority`` was
# absent, so a fresh empty ledger + positive caps constructed and sent
# (manager's real-SDK reproduction). Now: any positive cap without a
# (non-empty, dict) authority block — or with one that does not match
# the authoritative account — fails closed BEFORE the SDK is even
# imported: a fresh file can never re-open the frozen 498/500 headroom.
authority = config.get("authority")
if authority and (self.ledger.cap > 0 or self.llm_guard.cap > 0):
if self.ledger.cap > 0 or self.llm_guard.cap > 0:
if not isinstance(authority, dict) or not authority:
raise SemanticBudgetError("ledger_authority_required")
verify_ledger_authority(config.get("ledger_path", ""), authority)
try:
from mem0 import Memory # noqa: PLC0415 — fixed install, lazy by design
except ImportError as exc: # fail closed, never a silent fallback
raise RuntimeError("mem0_not_installed") from exc
# Unified provider-exit gate: the SDK's internal extraction LLM is a real
# provider exit like any other; when the product hands its router guard
# over, every extraction call also reserves request count + conservative
@@ -717,6 +762,13 @@ class Mem0Backend:
Reads the vector store's own payload (src_group anchor) — a crashed or
restarted worker finds what earlier attempts persisted, so recovery
resends only the missing rows and never duplicates completed ones.
HEL-885: every add carries a PER-ROW anchor, so each stored point
vouches for exactly the one message in its own ``turn_roles`` — vector
existence never claims other rows of the batch are done (that whole-
batch-anchor claim was the HEL-882 permanent-loss defect). Rows whose
extraction legitimately stored nothing carry no point; their durable
completion proof is the caller-side ``sem_row_done`` record.
"""
try:
from qdrant_client import models as qmodels # noqa: PLC0415 — lazy, pinned SDK
@@ -744,48 +796,65 @@ class Mem0Backend:
break
return done
async def add_group(self, rows: list[dict], anchor: dict, batch_key: str) -> list[dict]:
async def add_group(self, rows: list[dict], anchor: dict, batch_key: str, *,
skip: set[int] | frozenset[int] = frozenset(),
on_row_done=None) -> list[dict]:
"""One entry per row, each carrying that row's own anchor (HEL-885).
``skip`` holds message_ids already proven complete (vector-store
anchors ∪ the durable ``sem_row_done`` record) — those rows are never
re-sent. ``on_row_done(row, entries)`` fires in the worker thread as
soon as one row's add fully finished (empty extraction included), so
the caller can durably record per-row progress even if the process
dies mid-batch.
"""
# Fail closed before ANY real work while either budget is unapproved:
# a 2.2.1 add always spends ≥1 embed item and 1 extraction LLM call.
self.ledger.check(1)
self.llm_guard.check(1)
def _add() -> list[dict]:
# Recovery skips rows whose entries already live in the vector store
# (crash/restart/budget-exhaustion resume); the anchor sent with the
# remaining rows equals exactly that send's input set.
done = self.completed_message_ids(batch_key)
# Vector-store anchors are ALWAYS consulted (direct callers get the
# same no-resend guarantee); ``skip`` adds the caller's durable
# row records for rows that legitimately stored no vector.
done = self.completed_message_ids(batch_key) | set(skip)
send = [r for r in rows if r["message_id"] not in done]
if not send:
return []
send_anchor = {
"turn_ids": [r["turn_id"] for r in send],
"turn_roles": [[r["turn_id"], r["role"], r["message_id"]] for r in send],
"input_sha256": anchor_fingerprint(send),
}
failures_before = list(getattr(self._failures, "list", []) or [])
created = []
for row in send:
# Per-row anchor: this add's input set is exactly this row.
row_anchor = {
"turn_ids": [row["turn_id"]],
"turn_roles": [[row["turn_id"], row["role"], row["message_id"]]],
"input_sha256": anchor_fingerprint([row]),
}
failures_before = len(getattr(self._failures, "list", []) or [])
result = self._memory.add(
row["text"], metadata={"src_group": batch_key, **send_anchor},
row["text"], metadata={"src_group": batch_key, **row_anchor},
user_id=self.user_scope,
)
# mem0 2.2.1 swallows per-text embed failures into empty
# results (phase-3/7 fallback loops). An empty extraction is
# legitimate only when no embed/refusal happened during THIS
# row's add: if our guards refused anything, that refusal is
# the truth, not the empty list.
failures = (getattr(self._failures, "list", []) or [])[failures_before:]
if failures:
raise SemanticBudgetError(f"semantic_add_swallowed_failure:{failures[0]}")
items = result.get("results") if isinstance(result, dict) else result
if isinstance(items, dict):
items = [items]
row_entries = []
for item in items or []:
if not isinstance(item, dict):
continue
entry = {"id": item.get("id"), "data": (item.get("memory") or "")}
if entry["id"]:
created.append(entry)
# mem0 2.2.1 swallows per-text embed failures into empty results
# (phase-3/7 fallback loops). An empty extraction is legitimate only
# when no embed/refusal happened: if our guards refused anything
# during this add, that refusal is the truth, not the empty list.
failures = (getattr(self._failures, "list", []) or [])[len(failures_before):]
if failures:
raise SemanticBudgetError(f"semantic_add_swallowed_failure:{failures[0]}")
row_entries.append(entry)
created.extend(row_entries)
if on_row_done is not None:
on_row_done(row, row_entries)
return created
return await asyncio.to_thread(_add)
@@ -949,6 +1018,23 @@ class SemanticMemory:
"WHERE id=?", (owner, now + 40000, job["id"]),
)
state, reason, entry_ids = "failed", "extraction_failed", None
row_entries: list[str] = []
def _row_done(row: dict, entries: list[dict]) -> None:
"""Durable per-row completion, written inside the worker thread the
moment a row's add fully finished (HEL-885) — a legitimately empty
extraction stored no vector, so this record is its only resend
proof; a crash between rows can never resend a finished row."""
with self.store.conn:
self.store.execute(
"INSERT OR IGNORE INTO sem_row_done(batch_id,message_id,entry_ids,done_at) "
"VALUES(?,?,?,?)",
(job["id"], row["message_id"],
json.dumps([e["id"] for e in entries], ensure_ascii=False),
int(self.clock() * 1000)),
)
row_entries.extend(e["id"] for e in entries if e["id"])
try:
anchor = json.loads(job["anchor_json"])
rows = []
@@ -963,8 +1049,15 @@ class SemanticMemory:
"role": role, "text": message["text"]})
if anchor_fingerprint(rows) != anchor["input_sha256"]:
raise ValueError("source_mismatch")
created = await self.backend.add_group(rows, anchor, job["batch_key"])
entry_ids = json.dumps([e["id"] for e in created], ensure_ascii=False)
# Resume truth = vector-store anchors ∪ durable per-row records.
done_ids = self.backend.completed_message_ids(job["batch_key"])
done_ids |= {r["message_id"] for r in self.store.query(
"SELECT message_id FROM sem_row_done WHERE batch_id=?", (job["id"],))}
created = await self.backend.add_group(
rows, anchor, job["batch_key"], skip=done_ids, on_row_done=_row_done)
row_entries.extend(e["id"] for e in created if e["id"] not in row_entries)
entry_ids = json.dumps(
list(dict.fromkeys(row_entries)), ensure_ascii=False)
state, reason = "done", None
except Exception as exc: # noqa: BLE001 — job isolation, reason recorded
reason = str(exc) or exc.__class__.__name__
@@ -983,6 +1076,14 @@ class SemanticMemory:
return
log.warning("semantic batch %s failed: %s", job["id"], reason)
finally:
if state == "done":
# entry_ids = every entry this batch ever created, across all
# its partial attempts (sem_row_done holds the earlier ticks').
recorded = [json.loads(r["entry_ids"] or "[]") for r in self.store.query(
"SELECT entry_ids FROM sem_row_done WHERE batch_id=?", (job["id"],))]
entry_ids = json.dumps(
list(dict.fromkeys([e for lst in recorded for e in lst] + row_entries)),
ensure_ascii=False)
with self.store.conn:
self.store.execute(
"UPDATE sem_batch SET state=?,reason=?,entry_ids=?,owner=NULL,finished_at=? "
+9 -1
View File
@@ -182,7 +182,15 @@ def test_total_tokens_cap_blocks_before_daily_wall(tmp_path):
with TestClient(app) as client:
create_companion(client)
guard = app.state.life.router.guard
guard.record(4999, "review_almost_total") # one-shot total nearly spent
# HEL-885: the one-shot TOTAL must be booked in budget_total — the old
# setup used record(), which only writes the DAILY table, and passed
# merely because total_tokens_used() CRASHED on the then-empty
# budget_total (TypeError surfaced as chat.error). Book the total
# honestly now that the read is fixed.
with guard.store.conn:
guard.store.execute(
"INSERT INTO budget_total VALUES('total_tokens',4999) "
"ON CONFLICT(key) DO UPDATE SET value=4999")
calls_before = len(provider.calls)
assert [e["t"] for e in events(turn(client, "还会发吗"))] == ["chat.error"]
assert len(provider.calls) == calls_before
+30 -1
View File
@@ -99,13 +99,15 @@ class FakeLlm:
class FakeMemory:
instances = []
_seq_counter = 0 # unique entry ids across instances (restart-safe, like UUIDs)
def __init__(self, embedder, collection):
self.embedding_model = embedder
self.vector_store = _FakeVectorStore()
self.llm = FakeLlm()
self.collection = collection
self.seq = 0
FakeMemory._seq_counter += 1
self.seq = FakeMemory._seq_counter
@classmethod
def from_config(cls, config):
@@ -146,13 +148,36 @@ def _sys_modules():
def _make_ledger(path):
"""Test ledger with a synthetic authority prefix row (HEL-885).
The seeded row is deliberately an UNCOUNTED kind with 0 items, so guard
``used()`` stays 0 — it only anchors the file identity for the required
``authority`` block (product defaults are NOT relaxed; tests must hand the
backend an explicit synthetic authority, per the HEL-885 review).
"""
conn = sqlite3.connect(str(path))
conn.execute("CREATE TABLE embed_ops (id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT, pid INTEGER,"
" kind TEXT, items INTEGER, note TEXT)")
conn.execute(
"INSERT INTO embed_ops(ts,pid,kind,items,note) VALUES"
"('2026-01-01T00:00:00Z',1,'authority_prefix',0,'authority:synthetic-test-prefix')")
conn.commit()
conn.close()
def synthetic_authority(ledger_path, *, prefix_rows: int = 1, min_used: int = 0) -> dict:
"""Explicit synthetic authority over a test ledger's frozen prefix."""
import hashlib
rows = sqlite3.connect(str(ledger_path)).execute(
"SELECT ts,pid,kind,items,note FROM embed_ops ORDER BY id LIMIT ?",
(prefix_rows,),
).fetchall()
digest = hashlib.sha256(
"\n".join("|".join(str(c) for c in row) for row in rows).encode()).hexdigest()
return {"prefix_rows": prefix_rows, "prefix_sha256": digest, "min_used": min_used}
@pytest.fixture
def fake_mem0(monkeypatch, tmp_path):
ledger_path = tmp_path / "ledger.sqlite3"
@@ -197,6 +222,10 @@ def backend_for(tmp_path, fake_mem0, *, embedder=None, **overrides):
"llm_cap": 10,
}
config.update(overrides)
if config.get("embed_cap", 0) > 0 or config.get("llm_cap", 0) > 0:
# HEL-885: positive caps REQUIRE an explicit (synthetic) authority —
# the product default refuses without one; tests never relax that.
config.setdefault("authority", synthetic_authority(config["ledger_path"]))
# each backend wraps a fresh embedder, exactly like real from_config instances
sys.modules["mem0"]._TEST_EMBEDDER = embedder or FakeEmbedder()
return Mem0Backend(config)
+330
View File
@@ -0,0 +1,330 @@
"""HEL-885 rework regressions: the manager's independent-review failures.
Each case first mirrors the exact reproduction from HEL-885's review (real
public user+companion queue; cross-day settlement; aged unknown reservation;
missing authority; per-row progress), then proves the fix. Fake-SDK mirrors
run here; the pinned real install is covered live by
tools/verify_semantic_recovery_hel885.py (deterministic boundary, loopback).
"""
import asyncio
import json
import sqlite3
import pytest
from test_mem0_backend import POINT_LOG, FakeLlm, FakeMemory, _make_ledger, synthetic_authority
from test_semantic_recovery_hel882 import config_for, install_fake
from cybergril.life.model import PersistentBudget
from cybergril.memory.semantic import (
EmbedLedgerGuard,
LlmLedgerGuard,
Mem0Backend,
SemanticBudgetError,
SemanticMemory,
)
from cybergril.memory.store import Store
from cybergril.think.budget import BudgetConfig, BudgetGuard
def seed_public_two_row_turn(store, turn_id, user_text, companion_text):
"""The manager's partial-repro shape: a sent user row plus a judged+acked
(public) companion row in the same turn."""
with store.conn:
store.execute("INSERT INTO chat_message(ts,role,text,turn_id,status) VALUES"
"(1,'user',?,?,'sent')", (user_text, turn_id))
inserted = store.execute(
"INSERT INTO chat_message(ts,role,text,turn_id,status) VALUES"
"(2,'companion',?,?,'sent')", (companion_text, turn_id))
store.execute(
"INSERT INTO sharing_attempt(message_id,turn_id,epoch,token,text_hash,expires_at,"
"ack_at,candidates,state) VALUES(?,'"+turn_id+"',1,'t','h',9223372036854775806,"
"3,'[]','judged')", (inserted.lastrowid,))
return store.query_one("SELECT id FROM chat_message WHERE text=?", (user_text,))["id"], \
store.query_one("SELECT id FROM chat_message WHERE text=?", (companion_text,))["id"]
def llm_bodies(backend):
return [json.dumps(m, ensure_ascii=False) for m in backend._memory.llm.calls]
async def test_partial_batch_claims_only_the_row_actually_sent(tmp_path, monkeypatch):
"""Manager partial repro, fake mirror: llm_cap=1, two public rows — after
the first tick exactly ONE wire call happened, and the completed set is
the companion row ONLY (the HEL-882 whole-batch anchor claimed both).
Raising the cap resumes: done, and the user's original words really go
out — exactly once."""
install_fake(monkeypatch)
ledger = tmp_path / "partial.sqlite3"
_make_ledger(ledger)
backend = Mem0Backend(config_for(tmp_path, ledger, embed_cap=50, llm_cap=1))
store = Store(tmp_path / "partial.db")
user_id, companion_id = seed_public_two_row_turn(
store, "turn1", "合成用户原话甲", "合成已公开伴侣原话乙")
live = SemanticMemory(store, backend)
assert live.enqueue_uncovered() == 1
await live.tick()
job = dict(store.query_one("SELECT state,reason,entry_ids FROM sem_batch"))
assert job["state"] == "queued" and "budget" in (job["reason"] or "")
assert len(backend._memory.llm.calls) == 1 # one actual wire send, no retries
batch_key = store.query_one("SELECT batch_key FROM sem_batch")["batch_key"]
# THE defect: the first vector must not vouch for the unsent user row
assert backend.completed_message_ids(batch_key) == {companion_id}
points = backend._client.scroll(collection_name=backend.collection, with_payload=True,
limit=100)[0]
for point in points: # per-row anchors: each payload covers exactly one row
roles = point.payload.get("turn_roles") or []
assert len(roles) == 1
backend.llm_guard.cap = 5 # the manager raises the temporary cap
await live.tick()
after = dict(store.query_one("SELECT state,reason,entry_ids FROM sem_batch"))
assert after["state"] == "done" and json.loads(after["entry_ids"])
bodies = llm_bodies(backend)
assert sum("合成用户原话甲" in b for b in bodies) == 1 # actually sent, once
assert sum("合成已公开伴侣原话乙" in b for b in bodies) == 1 # never resent
assert backend.completed_message_ids(batch_key) == {user_id, companion_id}
store.close()
async def test_empty_extraction_row_is_done_and_never_resent(tmp_path, monkeypatch):
"""A row whose extraction legitimately returns nothing stores no vector —
it is still provably complete (sem_row_done) and never re-sent."""
install_fake(monkeypatch)
class EmptyForOneRow(FakeLlm):
def generate_response(self, messages=None, **kwargs):
user = messages[1]["content"] if messages and len(messages) > 1 else ""
if "甲" in user:
self.calls.append(messages)
return json.dumps({"memory": []}) # legitimate empty extraction
return super().generate_response(messages=messages, **kwargs)
class EmptyMemory(FakeMemory):
"""Parses the extraction like the real SDK: an empty memory list
stores NO point (the legitimate-empty path HEL-885 must prove done)."""
def __init__(self, embedder, collection):
super().__init__(embedder, collection)
self.llm = EmptyForOneRow()
def add(self, text, metadata=None, user_id=None):
if not user_id:
raise ValueError("filters must contain at least one of: user_id, agent_id, run_id")
self.embedding_model.embed(text, memory_action="search")
raw = self.llm.generate_response(messages=[
{"role": "system", "content": "extract"},
{"role": "user", "content": text},
], response_format={"type": "json_object"})
extracted = json.loads(raw).get("memory") or []
if not extracted:
return {"results": []} # nothing extracted → nothing stored
texts = [item["text"] for item in extracted]
self.embedding_model.embed_batch(texts, memory_action="add")
results = []
for one in texts:
self.seq += 1
entry_id = f"m{self.seq}"
bucket = POINT_LOG.setdefault(self.collection, {})
bucket[entry_id] = {"data": one, "user_id": user_id, **(metadata or {})}
results.append({"id": entry_id, "memory": one, "event": "ADD"})
return {"results": results}
install_fake(monkeypatch, EmptyMemory)
ledger = tmp_path / "empty.sqlite3"
_make_ledger(ledger)
backend = Mem0Backend(config_for(tmp_path, ledger, embed_cap=50, llm_cap=50))
store = Store(tmp_path / "empty.db")
seed_public_two_row_turn(store, "t9", "这一行提取为空甲", "这一行有提取乙")
live = SemanticMemory(store, backend)
live.enqueue_uncovered()
await live.tick()
batch = store.query_one("SELECT * FROM sem_batch")
assert batch["state"] == "done"
done = {r["message_id"] for r in store.query("SELECT message_id FROM sem_row_done")}
user_id = store.query_one("SELECT id FROM chat_message WHERE text='这一行提取为空甲'")["id"]
assert done == {user_id, store.query_one(
"SELECT id FROM chat_message WHERE text='这一行有提取乙'")["id"]}
# honest accounting: only the row that extracted something has an entry
assert len(json.loads(batch["entry_ids"])) == 1
# crash-style resume probe: batch requeued by hand, tick again — the
# empty-extraction row is skipped via sem_row_done, no second LLM send
with store.conn:
store.execute("UPDATE sem_batch SET state='queued',owner=NULL,lease_until=NULL")
calls_before = len(backend._memory.llm.calls)
await live.tick()
assert len(backend._memory.llm.calls) == calls_before # nothing re-sent
assert store.query_one("SELECT state FROM sem_batch")["state"] == "done"
store.close()
async def test_process_restart_resumes_only_the_missing_row(tmp_path, monkeypatch):
"""Crash after row 1 persisted: a NEW Store + NEW backend on the same
paths (process restart) resumes sending only the missing row."""
install_fake(monkeypatch)
ledger = tmp_path / "restart.sqlite3"
_make_ledger(ledger)
backend = Mem0Backend(config_for(tmp_path, ledger, embed_cap=50, llm_cap=1))
db_path = tmp_path / "restart.db"
store = Store(db_path)
seed_public_two_row_turn(store, "tr", "重启后第一行", "重启后第二行")
live = SemanticMemory(store, backend)
live.enqueue_uncovered()
await live.tick() # companion row done, user row budget-refused
batch_key = store.query_one("SELECT batch_key FROM sem_batch")["batch_key"]
store.close() # graceful close; durable state is what a crash leaves anyway
backend2 = Mem0Backend(config_for(tmp_path, ledger, embed_cap=50, llm_cap=50))
store2 = Store(db_path)
live2 = SemanticMemory(store2, backend2)
await live2.tick() # restart resumes the SAME batch
assert store2.query_one("SELECT state FROM sem_batch")["state"] == "done"
bodies = llm_bodies(backend2)
assert sum("重启后第一行" in b for b in bodies) == 1 # only the missing row
assert backend2.completed_message_ids(batch_key) == {
store2.query_one("SELECT id FROM chat_message WHERE text='重启后第一行'")["id"],
store2.query_one("SELECT id FROM chat_message WHERE text='重启后第二行'")["id"]}
store2.close()
async def test_concurrent_ticks_never_double_send(tmp_path, monkeypatch):
"""Two ticks racing on one batch: the BEGIN IMMEDIATE claim gives the
whole batch to one worker; every row is sent exactly once."""
install_fake(monkeypatch)
ledger = tmp_path / "race.sqlite3"
_make_ledger(ledger)
backend = Mem0Backend(config_for(tmp_path, ledger, embed_cap=50, llm_cap=50))
store = Store(tmp_path / "race.db")
seed_public_two_row_turn(store, "tc", "并发第一行", "并发第二行")
live = SemanticMemory(store, backend)
live.enqueue_uncovered()
await asyncio.gather(live.tick(), live.tick())
assert store.query_one("SELECT state FROM sem_batch")["state"] == "done"
bodies = llm_bodies(backend)
assert sum("并发第一行" in b for b in bodies) == 1
assert sum("并发第二行" in b for b in bodies) == 1
store.close()
def test_cross_day_settlement_books_on_the_reservation_day(tmp_path):
"""Manager midnight repro: day cap 100, hold 90 on day 1, actual 10
arriving on day 2 — day 1 settles to 10, day 2 stays 0 (never -80), and
a 150-token request the next day is refused."""
store = Store(tmp_path / "midnight.db")
guard = PersistentBudget(store, BudgetGuard(BudgetConfig(
daily_tokens=100, daily_requests={"dialogue": 10})))
guard._roll = lambda: None
guard.state.day = "2026-10-26"
hold = guard.acquire("dialogue_main", tokens=90)
guard.state.day = "2026-10-27"
guard.settle(hold, actual_tokens=10)
assert guard.used == 0 # new day untouched — no manufactured headroom
with pytest.raises(RuntimeError, match="budget_tokens_exhausted"):
guard.acquire("dialogue_main", tokens=150) # 150 > 100 cap → refused
rows = {r["day"]: r["used"] for r in store.query("SELECT * FROM token_usage")}
assert rows == {"2026-10-26": 10}
assert all(v >= 0 for v in rows.values())
store.close()
def test_cross_day_release_refunds_the_paying_day(tmp_path):
"""A provably-unsent hold reserved on day 1 and released on day 2 refunds
day 1 (and the one-shot totals) — day 2 never goes negative."""
store = Store(tmp_path / "release.db")
guard = PersistentBudget(store, BudgetGuard(BudgetConfig(
daily_tokens=100, daily_requests={"dialogue": 10}, total_tokens=500)))
guard._roll = lambda: None
guard.state.day = "2026-10-26"
hold = guard.acquire("dialogue_main", tokens=90)
guard.state.day = "2026-10-27"
guard.release(hold)
rows = {r["day"]: r["used"] for r in store.query("SELECT * FROM token_usage")}
assert rows == {"2026-10-26": 0}
assert guard.used == 0
assert guard.total_tokens_used() == 0
req = {r["day"]: r["requests"] for r in store.query("SELECT * FROM request_usage")}
assert req == {"2026-10-26": 0}
store.close()
def _backdate(path, table, row_id):
conn = sqlite3.connect(str(path))
with conn:
conn.execute(f"UPDATE {table} SET ts='2000-01-01T00:00:00Z' WHERE id=?", (row_id,))
conn.close()
def test_aged_unknown_llm_reservation_is_never_refunded(tmp_path):
"""Manager extra repro (llm side): a reservation that crashed before
settle is UNKNOWN — an hour (or a restart) later the cap is still spent;
no timeout quietly deletes the charging fact."""
ledger = tmp_path / "crash-llm.sqlite3"
llm = LlmLedgerGuard(ledger, cap=1)
pending = llm.reserve(1, "sent_but_crashed_before_settlement")
_backdate(ledger, "llm_ops", pending)
with pytest.raises(RuntimeError, match="semantic_llm_budget_exhausted"):
LlmLedgerGuard(ledger, cap=1).reserve(1, "second_send_after_restart")
reopened = LlmLedgerGuard(ledger, cap=1)
assert reopened.used() == 1 # the unknown consumption stayed billed
def test_aged_unknown_embed_reservation_is_never_refunded(tmp_path):
"""Same shared path, embed side (embed_ops) — unknown holds stay counted,
across process restarts; only an explicit unsent release removes one."""
ledger = tmp_path / "crash-embed.sqlite3"
_make_ledger(ledger)
guard = EmbedLedgerGuard(ledger, cap=1)
hold = guard.reserve(1, "embed_sent_then_crashed")
_backdate(ledger, "embed_ops", hold)
with pytest.raises(RuntimeError, match="semantic_embed_budget_exhausted"):
EmbedLedgerGuard(ledger, cap=1).reserve(1, "again_after_restart")
assert EmbedLedgerGuard(ledger, cap=1).used() == 1
# the explicit provably-unsent release still works — the ONLY removal path
fresh = EmbedLedgerGuard(ledger, cap=1)
fresh.release(hold)
assert fresh.used() == 0
async def test_positive_caps_without_authority_fail_closed(tmp_path, monkeypatch):
"""Manager partial repro's second finding: a fresh empty ledger + positive
caps + NO authority used to construct and send. Now every missing/empty/
malformed authority refuses construction before the SDK is touched."""
install_fake(monkeypatch)
fresh = tmp_path / "fresh-bypass.sqlite3"
conn = sqlite3.connect(str(fresh))
with conn:
conn.execute("CREATE TABLE embed_ops (id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT,"
" pid INTEGER, kind TEXT, items INTEGER, note TEXT)")
conn.execute("CREATE TABLE llm_ops (id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT,"
" pid INTEGER, kind TEXT, items INTEGER, note TEXT)")
conn.close()
def cfg(**extra):
base = config_for(tmp_path, fresh, embed_cap=0, llm_cap=0)
base.update(extra)
return base
with pytest.raises(SemanticBudgetError, match="ledger_authority_required"):
Mem0Backend(cfg(embed_cap=100, llm_cap=1)) # no authority at all
with pytest.raises(SemanticBudgetError, match="ledger_authority_required"):
Mem0Backend(cfg(embed_cap=100, llm_cap=1, authority={})) # empty block
with pytest.raises(SemanticBudgetError, match="ledger_authority_required"):
Mem0Backend(cfg(llm_cap=1, authority="see-notes")) # malformed
# a WRONG (non-matching) authority is refused by the prefix check
with pytest.raises(SemanticBudgetError):
Mem0Backend(cfg(embed_cap=100, llm_cap=1,
authority={"prefix_rows": 1, "prefix_sha256": "0" * 64, "min_used": 0}))
# zero caps stay constructible (fail-closed on WORK, not on construction)
zero = Mem0Backend(cfg(embed_cap=0, llm_cap=0))
with pytest.raises(RuntimeError, match="semantic_embed_budget_not_approved"):
await zero.add_group(
[{"message_id": 1, "turn_id": "t", "role": "user", "text": "x"}],
{"turn_ids": ["t"], "turn_roles": [], "input_sha256": "x"}, "z")
# the explicit synthetic authority path constructs (tests never relax the
# product default — they hand it a real prefix, as HEL-885 requires)
seeded = tmp_path / "seeded.sqlite3"
_make_ledger(seeded) # seeds the synthetic prefix row
okay = Mem0Backend(cfg(embed_cap=10, llm_cap=10, ledger_path=str(seeded),
authority=synthetic_authority(seeded)))
assert okay.backend == "mem0"
+7 -3
View File
@@ -116,15 +116,19 @@ async def test_gate_rejects_unshared_companion_row(store, sem):
make_public_companion(store, "t4")
sem.enqueue_uncovered()
await sem.tick()
hits = await sem.backend.search("面试")
# per-row anchors (HEL-885): the companion's own entry anchors only her row
hits = await sem.backend.search("辛苦")
assert sem.chain_resolve(hits[0])["resolved"]
# share state revoked at read time (attempt no longer acked) -> fail closed
with store.conn:
store.execute("UPDATE sharing_attempt SET ack_at=NULL WHERE turn_id='t4'")
chain = sem.chain_resolve(hits[0])
assert chain["reject_reason"] == "companion_unshared"
block = await sem.context("面试怎么样", [], 600)
assert "面试" not in block
block = await sem.context("辛苦了等什么", [], 600)
assert "辛苦" not in block
# her row's rejection never touches the user row's own entry
user_hit = (await sem.backend.search("面试"))[0]
assert sem.chain_resolve(user_hit)["resolved"]
async def test_context_respects_budget_and_excludes_turns(store, sem):
+47 -23
View File
@@ -21,7 +21,7 @@ import types
import pytest
from conftest import engineering_budget
from test_mem0_backend import POINT_LOG, FakeEmbedder, FakeMemory, _make_ledger
from test_mem0_backend import POINT_LOG, FakeEmbedder, FakeMemory, _make_ledger, synthetic_authority
from cybergril.memory.semantic import (
EmbedLedgerGuard,
@@ -117,6 +117,10 @@ def config_for(tmp_path, ledger, *, embed_cap=10, llm_cap=10, **extra):
"llm_cap": llm_cap,
}
config.update(extra)
if config.get("embed_cap", 0) > 0 or config.get("llm_cap", 0) > 0:
# HEL-885: positive caps REQUIRE an explicit synthetic authority; the
# product default (no authority) refuses — see test_rework_hel885.
config.setdefault("authority", synthetic_authority(ledger))
return config
@@ -217,40 +221,60 @@ async def test_mid_add_budget_swallow_requeues_then_resumes(tmp_path, monkeypatc
async def test_resume_never_loses_or_duplicates_rows(tmp_path, monkeypatch):
"""A crash mid-batch (row1 stored, row2 not) resumes sending ONLY row2."""
"""HEL-885 rewrite: the crash-split resume runs through the REAL product
path (real tick, real batch state) — no hand-planted single-row anchors.
The HEL-882 version planted exactly the anchor the product never wrote,
which masked the whole-batch-anchor defect the manager reproduced live."""
install_fake(monkeypatch)
ledger = tmp_path / "resume.sqlite3"
_make_ledger(ledger)
backend = Mem0Backend(config_for(tmp_path, ledger, embed_cap=50, llm_cap=50))
backend = Mem0Backend(config_for(tmp_path, ledger, embed_cap=50, llm_cap=1))
store = Store(tmp_path / "resume.db")
rows = seed_turn(store, "t2", "第一条原话")
seed_turn(store, "t2", "第一条原话")
with store.conn: # a shared companion row joins the turn
store.execute("INSERT INTO chat_message(ts,role,text,turn_id,status) VALUES"
"(2,'companion','第二条原话','t2','sent')")
row2 = store.query_one("SELECT id FROM chat_message WHERE text='第二条原话'")
rows.append({"message_id": row2["id"], "turn_id": "t2", "role": "companion", "text": "第二条原话"})
anchor = {"turn_ids": ["t2", "t2"], "turn_roles": [["t2", "user", rows[0]["message_id"]],
["t2", "companion", rows[1]["message_id"]]],
"input_sha256": "x"}
# simulate: attempt 1 stored row1's entry then died (e.g. crash before commit)
point_id = "pre-stored-1"
POINT_LOG.setdefault("cg_public", {})[point_id] = {
"data": "提取:第一条原话", "user_id": "cybergril",
"src_group": "chat_public:mem0:t2:key",
"turn_roles": [["t2", "user", rows[0]["message_id"]]],
"input_sha256": "x",
}
created = await backend.add_group(rows, anchor, "chat_public:mem0:t2:key")
texts_added = [backend.payload(e["id"])["data"] for e in created]
assert len(created) == 1 # only the missing row was sent
assert "第二条原话" in texts_added[0]
completed = backend.completed_message_ids("chat_public:mem0:t2:key")
assert completed == {rows[0]["message_id"], rows[1]["message_id"]}
store.execute(
"INSERT INTO sharing_attempt(message_id,turn_id,epoch,token,text_hash,expires_at,"
"ack_at,candidates,state) VALUES(?,'t2',1,'t','h',9223372036854775806,"
"1700000001000,'[]','judged')", (row2["id"],))
semantic = SemanticMemory(store, backend)
semantic.enqueue_uncovered()
await semantic.tick() # llm_cap=1: companion row spends it, user row refuses
first = dict(store.query_one("SELECT state,reason FROM sem_batch"))
assert first["state"] == "queued" and "budget" in (first["reason"] or "")
# per-row anchors: the ONE stored vector vouches for its own row ONLY
assert backend.completed_message_ids("chat_public:mem0:t2:" + _batch_digest(store)) == {
row2["id"]}
row1_id = store.query_one("SELECT id FROM chat_message WHERE text='第一条原话'")["id"]
done_rows = {r["message_id"] for r in store.query("SELECT message_id FROM sem_row_done")}
assert done_rows == {row2["id"]}
backend.llm_guard.cap = 50 # budget returns; the SAME batch resumes
await semantic.tick()
after = dict(store.query_one("SELECT state,reason,entry_ids FROM sem_batch"))
assert after["state"] == "done" and json.loads(after["entry_ids"])
# both rows actually sent, each EXACTLY once — no lost row, no resend
sent = [json.dumps(m, ensure_ascii=False) for m in backend._memory.llm.calls]
assert sum("第一条原话" in body for body in sent) == 1
assert sum("第二条原话" in body for body in sent) == 1
completed = backend.completed_message_ids("chat_public:mem0:t2:" + _batch_digest(store))
assert completed == {row1_id, row2["id"]}
# nothing left to send → idempotent no-op
assert await backend.add_group(rows, anchor, "chat_public:mem0:t2:key") == []
assert await backend.add_group(
[{"message_id": row1_id, "turn_id": "t2", "role": "user", "text": "第一条原话"},
{"message_id": row2["id"], "turn_id": "t2", "role": "companion", "text": "第二条原话"}],
{"turn_ids": ["t2", "t2"], "turn_roles": [], "input_sha256": "x"},
"chat_public:mem0:t2:" + _batch_digest(store)) == []
store.close()
def _batch_digest(store):
row = store.query_one("SELECT batch_key FROM sem_batch WHERE backend='mem0'")
return row["batch_key"].rsplit(":", 1)[1]
async def test_stub_batches_never_block_real_rebuild_and_real_never_resends(tmp_path, monkeypatch):
"""Review repro 3: stub→mem0 on the same archive reorganizes; a real done
batch is never re-enqueued nor re-sent."""
+16
View File
@@ -15,6 +15,9 @@
epoch+1、档案与消息仍在。
R6 mem0 同进程导入现状:lock-only 3.11 venv 的准确缺项(不下载;解决路径
见 deploy/hel874-release.md §4)。
R7 §4 依赖补齐命令的工具面(HEL-885):文档使用的落 wheel 命令
``python3 -m pip download`` 在本机确实存在(离线 --help 退出 0),
且已删除的旧命令 ``uv pip download`` 确实不存在(避免照抄无效命令)。
运行(在仓库根):
@@ -156,6 +159,19 @@ def main() -> None:
f"import mem0 → {REPORT['mem0_import_in_release_venv']} "
"(resolution path in deploy/hel874-release.md §4; no download in this check)")
# R7 — HEL-885: the §4 dependency-fill commands use tools that exist.
# Offline --help probes only: no network side effects, nothing downloaded.
pip_probe = subprocess.run(
[sys.executable, "-m", "pip", "download", "--help"], capture_output=True, text=True)
check("R7_pip_download_command_exists", pip_probe.returncode == 0,
f"{sys.executable} -m pip download --help → rc={pip_probe.returncode}")
with open(os.devnull, "wb") as devnull:
uv_rc = subprocess.run(["uv", "pip", "download", "--help"],
stdout=devnull, stderr=devnull).returncode
REPORT["removed_uv_pip_download_rc"] = uv_rc
check("R7_removed_uv_pip_download_still_absent", uv_rc != 0,
f"uv pip download --help → rc={uv_rc} (kept absent on purpose; do not re-document it)")
# offline models: echo providers, zero real gateway; ships-as-is caps (all 0)
offline_models = current / "config" / "models.offline-check.yaml"
offline_models.write_text(
+26 -1
View File
@@ -196,17 +196,37 @@ def install_facts() -> None:
def make_ledger(path: Path, cap: int, llm_cap: int) -> None:
"""Temp ledger + SYNTHETIC authority prefix row (HEL-885).
The seeded row is an uncounted kind with 0 items (guard ``used()`` stays
0); it exists because the product now REFUSES any positive cap without an
explicit authority block — test/verify harnesses hand it a synthetic one
over this prefix, never relaxing the production default.
"""
conn = sqlite3.connect(str(path))
with conn:
conn.execute("CREATE TABLE IF NOT EXISTS embed_ops (id INTEGER PRIMARY KEY AUTOINCREMENT,"
" ts TEXT, pid INTEGER, kind TEXT, items INTEGER, note TEXT)")
conn.execute("CREATE TABLE IF NOT EXISTS llm_ops (id INTEGER PRIMARY KEY AUTOINCREMENT,"
" ts TEXT, pid INTEGER, kind TEXT, items INTEGER, note TEXT)")
if conn.execute("SELECT count(*) FROM embed_ops").fetchone()[0] == 0:
conn.execute(
"INSERT INTO embed_ops(ts,pid,kind,items,note) VALUES"
"('2026-01-01T00:00:00Z',1,'authority_prefix',0,'authority:synthetic-verify-prefix')")
conn.close()
path.with_name(path.name + ".meta").write_text(
json.dumps({"embed_cap": cap, "llm_cap": llm_cap}), encoding="utf-8")
def synthetic_authority(ledger: Path, *, prefix_rows: int = 1, min_used: int = 0) -> dict:
rows = sqlite3.connect(str(ledger)).execute(
"SELECT ts,pid,kind,items,note FROM embed_ops ORDER BY id LIMIT ?",
(prefix_rows,)).fetchall()
digest = hashlib.sha256(
"\n".join("|".join(str(c) for c in row) for row in rows).encode()).hexdigest()
return {"prefix_rows": prefix_rows, "prefix_sha256": digest, "min_used": min_used}
def embed_used(path: Path) -> int:
conn = sqlite3.connect(str(path))
try:
@@ -235,7 +255,7 @@ def llm_used(path: Path) -> int:
def config_for(tmp: Path, ledger: Path, *, port: int, cap: int, llm_cap: int) -> dict:
return {
config = {
"gateway_base_url": f"http://127.0.0.1:{port}/v1",
"gateway_model": "hel874-deterministic",
"gateway_key": "hel874-local-test",
@@ -246,6 +266,11 @@ def config_for(tmp: Path, ledger: Path, *, port: int, cap: int, llm_cap: int) ->
"embed_cap": cap,
"llm_cap": llm_cap,
}
if cap > 0 or llm_cap > 0:
# HEL-885: positive caps require the explicit (synthetic) authority —
# the production default refuses without one.
config["authority"] = synthetic_authority(ledger)
return config
async def main_async() -> None:
+266
View File
@@ -0,0 +1,266 @@
#!/usr/bin/env python3
"""HEL-885 · 实际固定 SDK 恢复语义验证(零真实出站、零真实嵌入、零权威账写入)。
在固定 mem0ai 2.2.1(hel857-venv)+ 真实嵌入式 Qdrant + 真实 OpenAI 客户端上,
复现总管独立复审的四类失败并证明修复:确定性 512 维替身(TextEmbedding 构造前
安装)、127.0.0.1 假网关、临时账/库,全部同 tools/verify_mem0_real_hel874.py
的 HEL-882 边界口径。
S0 正 cap 缺 authority 拒绝构造(HEL-882 版曾成功构造并发送)。
S1 逐行锚点:llm_cap=1 双公开行 → 首个向量只证明自己那行;补预算后续跑,
用户原话真的发出且每行恰一次(总管 partial 复现的修复形态)。
S2 进程重启续跑:行1完成后重启(新 Store/新 Backend 同目录),只补缺行。
S3 租约回收与并发 tick:过期租约回收后处理;两个并发 tick 不双发。
S4 未知预占不超时退款(llm_ops 与 embed_ops 同路径):一小时前的未结算
预占重启后仍占额度;唯一删除路径是显式可证明未发送的 release。
证据 JSON 写 argv[1]。运行(仓库根,固定 venv):
/path/to/hel857-venv/bin/python tools/verify_semantic_recovery_hel885.py out.json
"""
from __future__ import annotations
import asyncio
import importlib.util
import json
import sqlite3
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
REPO = ROOT
sys.path.insert(0, str(REPO / "src"))
spec = importlib.util.spec_from_file_location('contract', REPO / 'tools/verify_mem0_real_hel874.py')
contract = importlib.util.module_from_spec(spec)
spec.loader.exec_module(contract)
contract.install_deterministic_boundary()
from cybergril.memory.semantic import ( # noqa: E402 — after boundary install
EmbedLedgerGuard,
LlmLedgerGuard,
Mem0Backend,
SemanticBudgetError,
SemanticMemory,
)
from cybergril.memory.store import Store # noqa: E402
REPORT: dict = {"head": "HEL-885 rework", "real_llm_requests": 0, "real_embeddings": 0,
"boundary": "deterministic 512 vectors, non-loopback forbidden"}
FAILURES: list[str] = []
def check(name: str, ok: bool, detail: str = "") -> bool:
REPORT.setdefault("checks", []).append({"name": name, "ok": bool(ok), "detail": detail})
if not ok:
FAILURES.append(f"{name}: {detail}")
return bool(ok)
def seed_two_row_turn(store: Store, turn_id: str, user_text: str, companion_text: str):
with store.conn:
store.execute("INSERT INTO chat_message(ts,role,text,turn_id,status) VALUES"
"(1,'user',?,?,'sent')", (user_text, turn_id))
inserted = store.execute(
"INSERT INTO chat_message(ts,role,text,turn_id,status) VALUES"
"(2,'companion',?,?,'sent')", (companion_text, turn_id))
store.execute(
"INSERT INTO sharing_attempt(message_id,turn_id,epoch,token,text_hash,expires_at,"
"ack_at,candidates,state) VALUES(?,'"+turn_id+"',1,'synthetic','synthetic',"
"999999,3,'[]','judged')", (inserted.lastrowid,))
return (store.query_one("SELECT id FROM chat_message WHERE text=?", (user_text,))["id"],
store.query_one("SELECT id FROM chat_message WHERE text=?", (companion_text,))["id"])
def bodies_since(mark: int) -> list[str]:
return [r["body"] for r in contract.FakeGateway.requests[mark:]]
async def scenario_partial_and_resume(tmp: Path, server, tag: str) -> dict:
"""S1 (+S2 restart inside): budget-fail split, resume, restart-safety."""
ledger = tmp / f"{tag}.sqlite3"
contract.make_ledger(ledger, cap=100, llm_cap=1)
cfg = contract.config_for(tmp, ledger, port=server.server_address[1],
cap=100, llm_cap=1)
cfg["vector_root"] = str(tmp / f"{tag}-vec")
cfg["history_root"] = str(tmp / f"{tag}-hist")
backend = Mem0Backend(cfg)
db = tmp / f"{tag}.db"
store = Store(db)
user_id, companion_id = seed_two_row_turn(
store, f"turn-{tag}", f"用户原话{tag}", f"伴侣公开原话{tag}")
live = SemanticMemory(store, backend)
live.enqueue_uncovered()
mark = len(contract.FakeGateway.requests)
await live.tick()
first_bodies = len(contract.FakeGateway.requests) - mark
first = dict(store.query_one("SELECT state,reason FROM sem_batch"))
batch_key = store.query_one("SELECT batch_key FROM sem_batch")["batch_key"]
completed = sorted(backend.completed_message_ids(batch_key))
points, _ = backend._client.scroll(collection_name=backend.collection,
with_payload=True, limit=100)
per_row = all(len((p.payload or {}).get("turn_roles") or []) == 1 for p in points)
check(f"S1_{tag}_first_tick_partial",
first["state"] == "queued" and "budget" in (first["reason"] or "")
and first_bodies == 1 and completed == [companion_id] and per_row,
f"state={first['state']} reason={first['reason']} http={first_bodies} "
f"completed={completed} points={len(points)} per_row_anchor={per_row}")
# S2 restart: close everything, reopen NEW instances on the same paths.
# Precise no-resend proof = HTTP count: only the user row still needs an
# add (one extraction request each). Raw text presence alone cannot count
# sends — mem0 legitimately carries earlier rows in the rolling history
# of every later extraction prompt.
store.close()
await backend.aclose()
cfg2 = dict(cfg)
backend2 = Mem0Backend(cfg2)
backend2.llm_guard.cap = 5 # the manager raises the temporary cap
store2 = Store(db)
live2 = SemanticMemory(store2, backend2)
mark2 = len(contract.FakeGateway.requests)
await live2.tick()
after = dict(store2.query_one("SELECT state,reason,entry_ids FROM sem_batch"))
bodies = bodies_since(mark2)
user_sent = any(f"用户原话{tag}" in b for b in bodies)
completed2 = sorted(backend2.completed_message_ids(batch_key))
check(f"S2_{tag}_restart_sends_only_missing_row",
after["state"] == "done" and len(bodies) == 1 and user_sent
and completed2 == sorted([user_id, companion_id])
and json.loads(after["entry_ids"] or "[]"),
f"state={after['state']} http={len(bodies)} user_sent={user_sent} "
f"completed={completed2} entry_ids={after['entry_ids']}")
store2.close()
await backend2.aclose()
return {"first_state": first["state"], "first_reason": first["reason"],
"completed_after_first": completed}
async def scenario_lease_and_concurrency(tmp: Path, server) -> None:
ledger = tmp / "lease.sqlite3"
contract.make_ledger(ledger, cap=100, llm_cap=10)
cfg = contract.config_for(tmp, ledger, port=server.server_address[1],
cap=100, llm_cap=10)
cfg["vector_root"] = str(tmp / "lease-vec")
cfg["history_root"] = str(tmp / "lease-hist")
backend = Mem0Backend(cfg)
db = tmp / "lease.db"
store = Store(db)
seed_two_row_turn(store, "turn-lease", "租约恢复第一行", "租约恢复第二行")
live = SemanticMemory(store, backend)
live.enqueue_uncovered()
# a worker died holding the lease: expired pending batch is recovered
with store.conn:
store.execute("UPDATE sem_batch SET state='pending',owner='dead',lease_until=1")
live.recover()
job = dict(store.query_one("SELECT state,reason FROM sem_batch"))
mark = len(contract.FakeGateway.requests)
await live.tick()
after_lease = dict(store.query_one("SELECT state FROM sem_batch"))
bodies = bodies_since(mark)
check("S3a_lease_expired_batch_recovers",
job["state"] == "failed" and job["reason"] == "lease_expired"
and after_lease["state"] == "done" and len(bodies) == 2
and all(any(t in b for b in bodies) for t in ("租约恢复第一行", "租约恢复第二行")),
f"recovered={job} state_after={after_lease['state']} http={len(bodies)}")
# concurrency: two ticks racing on one fresh batch never double-send
seed_two_row_turn(store, "turn-race", "并发第一行", "并发第二行")
live.enqueue_uncovered()
mark2 = len(contract.FakeGateway.requests)
await asyncio.gather(live.tick(), live.tick())
bodies2 = bodies_since(mark2)
races = store.query("SELECT state FROM sem_batch WHERE turn_id='turn-race'")
check("S3b_concurrent_ticks_single_send",
all(r["state"] == "done" for r in races) and len(bodies2) == 2
and all(any(t in b for b in bodies2) for t in ("并发第一行", "并发第二行")),
f"http={len(bodies2)} states={[r['state'] for r in races]}")
store.close()
await backend.aclose()
def scenario_aged_reservations(tmp: Path) -> None:
llm_path = tmp / "aged-llm.sqlite3"
llm = LlmLedgerGuard(llm_path, cap=1)
pending = llm.reserve(1, "sent_but_crashed_before_settlement")
with sqlite3.connect(str(llm_path)) as conn:
conn.execute("UPDATE llm_ops SET ts='2000-01-01T00:00:00Z' WHERE id=?", (pending,))
second_allowed = True
try:
LlmLedgerGuard(llm_path, cap=1).reserve(1, "second_after_restart")
except RuntimeError:
second_allowed = False
llm_used = LlmLedgerGuard(llm_path, cap=1).used()
check("S4a_aged_unknown_llm_reservation_kept",
(not second_allowed) and llm_used == 1,
f"second_allowed={second_allowed} used_after_restart={llm_used}")
embed_path = tmp / "aged-embed.sqlite3"
contract.make_ledger(embed_path, cap=1, llm_cap=0)
guard = EmbedLedgerGuard(embed_path, cap=1)
hold = guard.reserve(1, "embed_sent_then_crashed")
with sqlite3.connect(str(embed_path)) as conn:
conn.execute("UPDATE embed_ops SET ts='2000-01-01T00:00:00Z' WHERE id=?", (hold,))
embed_second = True
try:
EmbedLedgerGuard(embed_path, cap=1).reserve(1, "again")
except RuntimeError:
embed_second = False
embed_used = EmbedLedgerGuard(embed_path, cap=1).used()
EmbedLedgerGuard(embed_path, cap=1).release(hold) # explicit unsent release path
embed_after_release = EmbedLedgerGuard(embed_path, cap=1).used()
check("S4b_aged_unknown_embed_reservation_kept",
(not embed_second) and embed_used == 1 and embed_after_release == 0,
f"second_allowed={embed_second} used={embed_used} after_release={embed_after_release}")
def scenario_authority(tmp: Path) -> None:
fresh = tmp / "authority-fresh.sqlite3"
conn = sqlite3.connect(str(fresh))
with conn:
conn.execute("CREATE TABLE embed_ops (id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT,"
" pid INTEGER, kind TEXT, items INTEGER, note TEXT)")
conn.close()
base = {"gateway_base_url": "http://127.0.0.1:9/v1", "gateway_model": "x",
"vector_root": str(tmp / "a-vec"), "history_root": str(tmp / "a-hist"),
"collection": "cg_a", "ledger_path": str(fresh),
"embed_cap": 100, "llm_cap": 1}
refused = ""
try:
Mem0Backend(dict(base))
except SemanticBudgetError as exc:
refused = str(exc)
empty_block = ""
try:
Mem0Backend({**base, "authority": {}})
except SemanticBudgetError as exc:
empty_block = str(exc)
check("S0_positive_caps_without_authority_fail_closed",
refused == "ledger_authority_required" and empty_block == "ledger_authority_required",
f"missing={refused} empty={empty_block}")
async def main() -> None:
server = contract.start_gateway()
with tempfile.TemporaryDirectory(dir=ROOT / "tools" if (ROOT / "tools").is_dir()
else None) as raw_tmp:
tmp = Path(raw_tmp)
try:
scenario_authority(tmp)
await scenario_partial_and_resume(tmp, server, "p1")
await scenario_lease_and_concurrency(tmp, server)
scenario_aged_reservations(tmp)
finally:
server.shutdown()
server.server_close()
REPORT["gateway_requests_total"] = len(contract.FakeGateway.requests)
REPORT["failures"] = FAILURES
out = Path(sys.argv[1] if len(sys.argv) > 1 else "hel885-recovery-report.json")
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(REPORT, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps({"out": str(out), "failures": FAILURES,
"checks": {c["name"]: c["ok"] for c in REPORT.get("checks", [])}},
ensure_ascii=False, indent=2))
sys.exit(1 if FAILURES else 0)
asyncio.run(main())