阶段F第一部分: 冻结幂等键改(manifest,context)对——候选164双门通过后被CONTEXT_NOT_FROZEN阻闸的缺陷修复(迁移114+真实库测试)

This commit is contained in:
zizi 2026-08-23 14:15:09 +08:00
parent 864c2ca380
commit d9cf5e834c
4 changed files with 111 additions and 6 deletions

View File

@ -79,13 +79,16 @@ def persist_freeze(assemble_result, *, reference_work_id=None, reference_version
auth_json = json.dumps(authorization, ensure_ascii=False) if isinstance(authorization, dict) else None
with connect() as conn:
try:
# manifest_sha256 唯一:同一冻结重放幂等。表是 append-only,冲突只能回读,不能 UPDATE。
# (manifest, context) 对唯一:同一冻结重放幂等;同清单不同上下文的冻结各得其所。
# 表是 append-only,冲突只能回读,不能 UPDATE。
# WHY:检索清单可跨运行不变而上下文每次不同(预防合同/时间进 SHA),
# manifest 单键会让新上下文永远落不了库,接受前置检查按 context_sha 必查不到。
row = conn.execute(
"INSERT INTO example_context_freeze(work_id, target_chapter, as_of_chapter, manifest_sha256, "
"context_sha256, reference_work_id, reference_version, arm_config, sections, token_budget, "
"omitted_sources, authorization_snapshot, creator) "
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s::jsonb,%s::jsonb,%s,%s::jsonb,%s::jsonb,%s) "
"ON CONFLICT (manifest_sha256) DO NOTHING "
"ON CONFLICT (manifest_sha256, context_sha256) DO NOTHING "
"RETURNING id, manifest_sha256, context_sha256",
(work_id, target_chapter, as_of, manifest_sha, context_sha, reference_work_id,
reference_version, json.dumps(arm_config, ensure_ascii=False) if arm_config is not None else None,
@ -94,8 +97,8 @@ def persist_freeze(assemble_result, *, reference_work_id=None, reference_version
if not row:
row = conn.execute(
"SELECT id,manifest_sha256,context_sha256 FROM example_context_freeze "
"WHERE manifest_sha256=%s",
(manifest_sha,),
"WHERE manifest_sha256=%s AND context_sha256=%s",
(manifest_sha, context_sha),
).fetchone()
if not row:
raise RuntimeError("冻结幂等回读失败")

View File

@ -0,0 +1,18 @@
-- 114 冻结幂等键:manifest 单键 → (manifest, context) 对
--
-- WHY:检索清单(manifest)可以跨运行保持不变——例如知识检索全空、细纲与基线
-- 未变时,多次运行算出同一个 manifest_sha256;但冻结上下文每次不同
-- (预防合同、规则库版本、生成时间等进 contextSha256)。manifest_sha256
-- 唯一键使新上下文的冻结永远命中 ON CONFLICT DO NOTHING,落不了库;
-- 接受前置检查(acceptance_state)按 context_sha256 查询必查不到,
-- CONTEXT_NOT_FROZEN 阻断人接受通道。
--
-- (manifest, context) 对保持"同一冻结重放幂等":同一份冻结重放,两个
-- SHA 都相同,仍然幂等;不同上下文的冻结各得其所。
-- 2026-08-22/23 两阶段写手烟测实证:候选通过双门后被该缺陷挡在人闸前。
ALTER TABLE example_context_freeze
DROP CONSTRAINT IF EXISTS uk_example_context_freeze_manifest;
CREATE UNIQUE INDEX IF NOT EXISTS uk_example_context_freeze_manifest_context
ON example_context_freeze (manifest_sha256, context_sha256);

View File

@ -62,9 +62,18 @@
| 生成阶段模型发起探索工具调用,`--no-tools` 下无法解析导致挂死 | 写手角色合同写“动笔前按需使用授权工具自主取材”,与生成阶段“无工具”冲突;模型非确定性服从 | `writer.md` 改写为两阶段形态:探索阶段(白名单非空,只探索不写作)/ 生成阶段(白名单为空,禁止工具调用,单条成稿) |
| 生成会话被旧单阶段运行(带工具的探索+碎片写作)污染,诱导探索行为 | 生成复用旧会话 `writer-work{W}-ch{T}` | 生成会话独立标签 `writer-gen`(探索为 `writer-explore`) |
| 挂死不能无限阻塞 | 看门狗已存在(绝对超时 `maxDurationSeconds`,到点杀进程,`FRAMEWORK_TIMEOUT`) | 无需新增;本次人工提前清理仅为节省等待 |
| 令牌经命令行传入在进程表可见 | tmux 命令串内嵌环境变量赋值 | 运行结束后轮换 `MUSE_AI_NEW_API_TOKEN`;后续用环境文件或进程内注入,不进命令行 |
| 候选过双门后接受前置检查报 `CONTEXT_NOT_FROZEN`,人闸被阻 | 冻结幂等键为 `manifest_sha256` 单键;检索清单跨运行不变而上下文每次不同,新冻结永远 `ON CONFLICT DO NOTHING` 回读旧行 | 迁移 `db/ddl/114-example冻结幂等键-清单加上下文对.sql`:唯一键改 `(manifest, context)` 对;`persist_freeze` 同步改冲突键与回读条件;补落候选 164 冻结行后复验 `ACCEPTANCE_INTENT_READY`;新增零污染真实库测试 |
| 令牌经命令行传入在进程表可见 | tmux 命令串内嵌环境变量赋值 | 改用 600 权限环境文件注入;运行结束后轮换 `MUSE_AI_NEW_API_TOKEN` |
## 6. 未决(下一阶段)
## 6. 真实验证结果(2026-08-23)
`run-prod-work12-ch3-42beb650`(opus-5 high,指令留空):
- 探索阶段:约 2 分钟,5 份材料;生成阶段:无工具单条成稿,无碎片化;AI 味诊断 13 条。
- 机械门通过、语义检测通过;候选 164(candidate_version=9)落库 `state=passed, semantic_status=passed`。
- 冻结缺陷修复后接受前置检查复验:`ACCEPTANCE_INTENT_READY`——候选停在人闸,等人在决策通道接受/拒绝。
## 7. 未决(下一阶段)
- 两阶段真实烟测通过后:两阶段成为生产默认形态,单阶段派发降级为对照模式;角色合同与领域文档同步改写(写手 = 探索阶段 + 生成阶段)。
- 评委与抽取智能体的框架派发接入、直调链终态裁决仍按总 plan 阶段 F 推进。

View File

@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""冻结幂等键(manifest, context)对真实库集成测试。
example_context_freeze 是 append-only 表(触发器禁止 UPDATE/DELETE),
无法物理清理测试行;因此本测试零新增:回读一条既有冻结行,按其
manifest+context 原值重放,验证幂等回读同一行、不新增。
"同清单不同上下文各得其所"由 2026-08-23 两阶段烟测实证(候选 164 的
冻结在旧 manifest 单键下永远落不了库,迁移后落库成功并通过接受前置检查),
并由迁移 114 的 (manifest, context) 唯一索引在库级保证。
跑法(需可达 muse-example):
.venv/bin/python tests/skills/assemble-context/test_persist_freeze_db.py
"""
from __future__ import annotations
import pathlib
import sys
import unittest
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / ".agent" / "skills" / "assemble-context" / "scripts"
for path in (SCRIPT_DIR,):
if str(path) not in sys.path:
sys.path.insert(0, str(path))
from persist_context_freeze import persist_freeze # noqa: E402
from muse_db import connect # noqa: E402
class FreezeReplayIdempotencyDbTest(unittest.TestCase):
def test_replay_existing_freeze_is_idempotent_no_new_row(self):
with connect(readonly=True) as conn:
row = conn.execute(
"SELECT work_id, target_chapter, as_of_chapter, manifest_sha256, "
"context_sha256 FROM example_context_freeze ORDER BY id DESC LIMIT 1"
).fetchone()
self.assertIsNotNone(row, "需要至少一条既有冻结行")
work_id, target_chapter, as_of, manifest_sha, context_sha = row
assemble_result = {
"context": {
"workId": work_id,
"targetChapter": target_chapter,
"asOf": as_of,
"contextSnapshot": {
"manifestId": "sha256:" + manifest_sha,
"contextSha256": "sha256:" + context_sha,
},
"retrievalManifest": {"sources": []},
}
}
with connect(readonly=True) as conn:
before = conn.execute(
"SELECT count(*) FROM example_context_freeze WHERE manifest_sha256=%s",
(manifest_sha,),
).fetchone()[0]
out = persist_freeze(assemble_result)
self.assertEqual(out["status"], "frozen")
self.assertEqual(out["manifest_sha256"], manifest_sha)
self.assertEqual(out["context_sha256"], context_sha)
with connect(readonly=True) as conn:
after = conn.execute(
"SELECT count(*) FROM example_context_freeze WHERE manifest_sha256=%s",
(manifest_sha,),
).fetchone()[0]
self.assertEqual(after, before, "幂等重放不得新增行")
if __name__ == "__main__":
unittest.main()