阶段F第一部分: 冻结幂等键改(manifest,context)对——候选164双门通过后被CONTEXT_NOT_FROZEN阻闸的缺陷修复(迁移114+真实库测试)
This commit is contained in:
parent
864c2ca380
commit
d9cf5e834c
@ -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
|
auth_json = json.dumps(authorization, ensure_ascii=False) if isinstance(authorization, dict) else None
|
||||||
with connect() as conn:
|
with connect() as conn:
|
||||||
try:
|
try:
|
||||||
# manifest_sha256 唯一:同一冻结重放幂等。表是 append-only,冲突只能回读,不能 UPDATE。
|
# (manifest, context) 对唯一:同一冻结重放幂等;同清单不同上下文的冻结各得其所。
|
||||||
|
# 表是 append-only,冲突只能回读,不能 UPDATE。
|
||||||
|
# WHY:检索清单可跨运行不变而上下文每次不同(预防合同/时间进 SHA),
|
||||||
|
# manifest 单键会让新上下文永远落不了库,接受前置检查按 context_sha 必查不到。
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"INSERT INTO example_context_freeze(work_id, target_chapter, as_of_chapter, manifest_sha256, "
|
"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, "
|
"context_sha256, reference_work_id, reference_version, arm_config, sections, token_budget, "
|
||||||
"omitted_sources, authorization_snapshot, creator) "
|
"omitted_sources, authorization_snapshot, creator) "
|
||||||
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s::jsonb,%s::jsonb,%s,%s::jsonb,%s::jsonb,%s) "
|
"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",
|
"RETURNING id, manifest_sha256, context_sha256",
|
||||||
(work_id, target_chapter, as_of, manifest_sha, context_sha, reference_work_id,
|
(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,
|
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:
|
if not row:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT id,manifest_sha256,context_sha256 FROM example_context_freeze "
|
"SELECT id,manifest_sha256,context_sha256 FROM example_context_freeze "
|
||||||
"WHERE manifest_sha256=%s",
|
"WHERE manifest_sha256=%s AND context_sha256=%s",
|
||||||
(manifest_sha,),
|
(manifest_sha, context_sha),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
raise RuntimeError("冻结幂等回读失败")
|
raise RuntimeError("冻结幂等回读失败")
|
||||||
|
|||||||
18
db/ddl/114-example冻结幂等键-清单加上下文对.sql
Normal file
18
db/ddl/114-example冻结幂等键-清单加上下文对.sql
Normal 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);
|
||||||
@ -62,9 +62,18 @@
|
|||||||
| 生成阶段模型发起探索工具调用,`--no-tools` 下无法解析导致挂死 | 写手角色合同写“动笔前按需使用授权工具自主取材”,与生成阶段“无工具”冲突;模型非确定性服从 | `writer.md` 改写为两阶段形态:探索阶段(白名单非空,只探索不写作)/ 生成阶段(白名单为空,禁止工具调用,单条成稿) |
|
| 生成阶段模型发起探索工具调用,`--no-tools` 下无法解析导致挂死 | 写手角色合同写“动笔前按需使用授权工具自主取材”,与生成阶段“无工具”冲突;模型非确定性服从 | `writer.md` 改写为两阶段形态:探索阶段(白名单非空,只探索不写作)/ 生成阶段(白名单为空,禁止工具调用,单条成稿) |
|
||||||
| 生成会话被旧单阶段运行(带工具的探索+碎片写作)污染,诱导探索行为 | 生成复用旧会话 `writer-work{W}-ch{T}` | 生成会话独立标签 `writer-gen`(探索为 `writer-explore`) |
|
| 生成会话被旧单阶段运行(带工具的探索+碎片写作)污染,诱导探索行为 | 生成复用旧会话 `writer-work{W}-ch{T}` | 生成会话独立标签 `writer-gen`(探索为 `writer-explore`) |
|
||||||
| 挂死不能无限阻塞 | 看门狗已存在(绝对超时 `maxDurationSeconds`,到点杀进程,`FRAMEWORK_TIMEOUT`) | 无需新增;本次人工提前清理仅为节省等待 |
|
| 挂死不能无限阻塞 | 看门狗已存在(绝对超时 `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 推进。
|
- 评委与抽取智能体的框架派发接入、直调链终态裁决仍按总 plan 阶段 F 推进。
|
||||||
|
|||||||
75
tests/skills/assemble-context/test_persist_freeze_db.py
Normal file
75
tests/skills/assemble-context/test_persist_freeze_db.py
Normal 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()
|
||||||
Loading…
x
Reference in New Issue
Block a user