session-mechanism:接上「遇阻碍 ⇒ 置阻碍 ⇒ 机制暂停」+ 授权检查会话自解除孤儿锁
用户 10-09 两条令:
①「如果 工作区 项目机制 对应的会话异常 导致未解锁的情况下,可以授权 检查会话自解除孤儿锁」
②「其他类型 阻碍 遇到阻碍修改目标状态 暂停机制 让用户决策,不是有这个规则吗」
起因(contentm_agent 实测):一把域锁的持有者会话被 terminated、锁遗留未释放 ⇒ 目标第 4 步 blocked;
R9 一律禁删锁 ⇒ 检查会话连开 11 棒(约 5 小时)每棒都撞同一面墙、零派活,每 30 分钟一棒。
拆开看是三条腿互相锁死:闸③「队列非空」对一条 blocked 恒真;闸① 2026-10-04 收窄时把
sessions-ended 整条腿摘了出去(置了「阻碍」也照样建);结果检查 prompt 又明写「你没有改目标状态的出口」
⇒ 看得见的没权、有权的(目标检查要队列为空)看不见。
改动
- 新增 scripts/lock/orphan-lock.py:孤儿锁「判定 + 解除」的唯一执行面。
判据查宿主库 sessions.status(只读 SQL):working 一律不动;terminated/error/archived 可解除;
completed 须静默 >=30 分钟;查不到 sid / 库读不到一律不动(fail-closed)。
- collabd.py
· 结果检查 prompt 开「一个」状态出口:--set-life 阻碍 --by "<会话名>" --check-why "<一句现状>"
("为什么"的旗标是 --check-why,不是 --why —— 后者是 --retire 的,写错会被静默忽略);
同时把「写明阻碍」的两个出口钉死:台账 --report ... --state blocked --reason +
NEED-USER.md 里明确喊「需用户介入」。
· 闸① 改两档:queue-empty 原样(非「进行中」不建);sessions-ended「只拦『阻碍』」,
「已完成」仍放行(不把 2026-10-04 修好的病搬回来)。docstring 五道闸 -> 六道闸。
· 两条检查 prompt 都加了孤儿锁判定钩子(判「持有者已退出」才可自解除;判「活着」一律不动)。
- R9 条文加「唯一例外」:工作区 CODEBUDDY.md §3(定义处)、references/rules.md 新增 §10、SKILL.md,
以及技能包与文档库两份 handoff-guard.sh 的「抢域锁失败」提示。
- selftest.py:新增两个用例(孤儿锁 16 项,含「working 不许动」的变异对照;阻碍档 6 项,
含「已完成仍要建」的变异对照),并把写在两处的同一判据收成一处(改一处漏一处的实测教训)。
全套 PASS 108 / FAIL 0。
- references/manifest.md:按「改完包必跑」重算(77 份文件,语法失败 0)。
不在本仓(各自仓内提交):工作区 CODEBUDDY.md、文档库 CODEBUDDY.md 与 07-scripts/handoff-guard.sh。
This commit is contained in:
1 parent
c36a3f4a50
commit
c76f5be36c
7 files changed
+532
-29
No files matched your search
@@ -814,9 +814,15 @@ def t_two_kinds_of_check_agent():
|
||||
rows.append(("⛔ 两套 prompt **不是同一份**(实得 %s)" % (pr == pg), pr != pg))
|
||||
rows.append(("结果检查:标题是「结果检查会话」(%s)", "【结果检查会话" in pr))
|
||||
rows.append(("目标检查:标题是「目标检查会话」(%s)", "【目标检查会话" in pg))
|
||||
# 职责边界:结果检查 ⛔ 不许有改目标状态的出口
|
||||
rows.append(("结果检查 ⛔ 不含「改目标状态」的出口", "没有「改目标状态」" in pr))
|
||||
rows.append(("⛔ 结果检查里不该出现 `--set-life`", "--set-life" not in pr))
|
||||
# 🔴🔴 2026-10-09 **改口径**(用户令逐字:「**其他类型 阻碍 遇到阻碍修改目标状态 暂停机制 让用户决策**」):
|
||||
# 旧断言是「结果检查 ⛔ 一个改状态的出口都不许有」—— 那正是空转病根的一半:
|
||||
# **看得见阻碍的只有结果检查**(它靠"队列非空"触发),而有权改状态的目标检查要
|
||||
# **队列为空**才触发 ⇒ 队列里躺着 `blocked` 时它永不触发 ⇒ 两条腿互相锁死。
|
||||
# ✅ 现在两腿**各有一个**出口:结果检查=「阻碍」档、目标检查=「已完成」档。
|
||||
# ⚠️ 两腿"允许写哪个状态"的**旗标级校验只保留一处**(`t_check_prompt_cmds_exist`)——
|
||||
# 本轮实测教训:同一判据写两处 ⇒ 改一处漏一处,下面这两行就是这么报红的。
|
||||
rows.append(("结果检查:写进用户口径「遇阻碍 ⇒ 暂停机制 ⇒ 让用户决策」",
|
||||
"机制暂停" in pr and "让用户决策" in pr))
|
||||
# 目标检查必须有改状态的出口
|
||||
rows.append(("目标检查含 `--set-life 已完成` 出口", "--set-life 已完成" in pg))
|
||||
rows.append(("目标检查讲了三路完成度(含 `acceptance_state`)", "acceptance_state" in pg))
|
||||
@@ -964,8 +970,22 @@ def t_check_prompt_cmds_exist():
|
||||
rows.append(("目标检查:`--set-life` 与值之间有空格",
|
||||
"--set-life 已完成" in p))
|
||||
else:
|
||||
rows.append(("结果检查:⛔ 不含 `--set-life`(那是目标检查的出口)",
|
||||
"--set-life" not in p))
|
||||
# 🔴🔴 2026-10-09 改口径(用户令:「遇到阻碍修改目标状态 暂停机制 让用户决策」):
|
||||
# 旧断言是「结果检查 ⛔ 一个 `--set-life` 都不许有」—— 那正是病根的一半:
|
||||
# 结果检查是**唯一能看见 `blocked`** 的那条腿(它靠"队列非空"触发),
|
||||
# 而有权改状态的「目标检查」要**队列为空**才触发 ⇒ 队列里躺着 blocked 时它永不触发
|
||||
# ⇒ **看得见的没权、有权的看不见** ⇒ 只剩每 30 分钟空转一棒(实测连开 11 棒)。
|
||||
# ✅ 现口径:给结果检查**一个**出口(只许「阻碍」),⛔ 「已完成」仍归目标检查。
|
||||
rows.append(("结果检查:**有一个**状态出口 —— 只许 `--set-life 阻碍`",
|
||||
"--set-life 阻碍" in p))
|
||||
rows.append(("结果检查:⛔ 不许 `--set-life 已完成`(越权)",
|
||||
"--set-life 已完成" not in p))
|
||||
rows.append(("结果检查:`--set-life` 的「为什么」旗标写对(`--check-why`,⛔ 不是 `--why`)",
|
||||
"--check-why" in p and "--set-life 阻碍 --by" in p))
|
||||
rows.append(("结果检查:写明写完**必须回读** `goal.json` 确认",
|
||||
"必须回读" in p))
|
||||
rows.append(("结果检查:写明**谁把它改回「进行中」**(⛔ 不留给用户空等)",
|
||||
"--set-life 进行中" in p))
|
||||
return rows
|
||||
|
||||
|
||||
@@ -7438,5 +7458,159 @@ def t_voice_core():
|
||||
]
|
||||
|
||||
|
||||
@case("🔴 孤儿锁:授权检查会话自解除 —— 但**只**放行「持有者已退出」这一档")
|
||||
def t_orphan_lock():
|
||||
"""🔴 2026-10-09 立(用户令逐字:「如果 工作区 项目机制 对应的会话异常 导致未解锁的情况下,
|
||||
可以授权 检查会话自解除孤儿锁」)。
|
||||
|
||||
病根:`contentm_agent` 一把孤儿锁(持有者会话已被 terminated)把目标第 4 步挡住,
|
||||
而 R9「AI 一律不得删锁、不得接管」只有行为约定、**没有任何执行面** ⇒ 检查会话连开 11 棒
|
||||
(约 5 小时)每棒都撞同一面墙。⇒ 授权必须落成**一条带判据的命令**,⛔ 不靠 AI 记得去取证。
|
||||
|
||||
⚠️ 本用例**非有变异对照不可**:只验「terminated 能解除」的话,一个「见锁就删」的野实现
|
||||
也照样全绿 —— 那正是 R9 要防的事。⇒ 必须同时验「`working` 的锁一把都不许动」。
|
||||
"""
|
||||
import importlib.util
|
||||
import shutil as _sh
|
||||
import sqlite3 as _sq
|
||||
import time as _t
|
||||
|
||||
spec = importlib.util.spec_from_file_location("orphan_lock", HERE / "lock" / "orphan-lock.py")
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(m)
|
||||
|
||||
now_ms = int(_t.time() * 1000)
|
||||
old_ms = now_ms - 40 * 60 * 1000 # 40 分钟前
|
||||
|
||||
unit = [
|
||||
("持有者 `working` ⇒ ⛔ 不动(R9 原样)", m._judge("working", now_ms, 30.0)[0] is False),
|
||||
("持有者 `terminated` ⇒ 可解除", m._judge("terminated", 0, 30.0)[0] is True),
|
||||
("持有者 `error` ⇒ 可解除", m._judge("error", 0, 30.0)[0] is True),
|
||||
("持有者 `archived` ⇒ 可解除", m._judge("archived", 0, 30.0)[0] is True),
|
||||
("`completed` 但刚收工(0 分钟)⇒ ⛔ 不动(别掐掉随时会续聊的会话)",
|
||||
m._judge("completed", now_ms, 30.0)[0] is False),
|
||||
("`completed` 且已静默 40 分钟 ⇒ 可解除",
|
||||
m._judge("completed", old_ms, 30.0)[0] is True),
|
||||
("`completed` 但读不到活动时刻 ⇒ ⛔ 不动(fail-closed)",
|
||||
m._judge("completed", None, 30.0)[0] is False),
|
||||
("查不到该会话(status 空)⇒ ⛔ 不动", m._judge("", None, 30.0)[0] is False),
|
||||
("状态不在已知档 ⇒ ⛔ 不动(fail-closed)", m._judge("某个没见过的档", None, 30.0)[0] is False),
|
||||
]
|
||||
|
||||
# ── 端到端:真造锁目录 + 真造宿主库,跑真脚本 ────────────────────────
|
||||
T = TEST_WS / "orphan"
|
||||
_sh.rmtree(T, ignore_errors=True)
|
||||
T.mkdir(parents=True, exist_ok=True)
|
||||
db = T / "host.db"
|
||||
con = _sq.connect(str(db))
|
||||
con.execute("create table sessions (id text, status text, last_activity_at integer)")
|
||||
con.execute("insert into sessions values (?,?,?)", ("deadbeef-0000-0000-0000-000000000001", "terminated", now_ms))
|
||||
con.execute("insert into sessions values (?,?,?)", ("alivebee-0000-0000-0000-000000000002", "working", now_ms))
|
||||
con.commit()
|
||||
con.close()
|
||||
|
||||
def _mk(d, owner, sid):
|
||||
p = T / d
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
(p / "OWNER").write_text("%s\n开始:10-09 00:48\n会话:%s\nME:%s\n" % (owner, sid, owner),
|
||||
encoding="utf-8")
|
||||
return p
|
||||
|
||||
_mk("lock-dead", "执行会话-甲", "deadbeef-0000-0000-0000-000000000001")
|
||||
_mk("lock-alive", "执行会话-乙", "alivebee-0000-0000-0000-000000000002")
|
||||
_mk("lock-nosid", "执行会话-丙", "未取到") # 旧锁没记 sid
|
||||
|
||||
def _run(hostdb):
|
||||
env = dict(os.environ)
|
||||
env["DSH_HOST_DB"] = str(hostdb)
|
||||
env["COLLABD_NO_ENSURE"] = "1"
|
||||
p = subprocess.run([PY, "-u", str(HERE / "lock" / "orphan-lock.py"),
|
||||
"--locks-root", str(T)],
|
||||
cwd=str(HERE), stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, env=env, timeout=60, errors="replace")
|
||||
return p.returncode, p.stdout or ""
|
||||
|
||||
rc, out = _run(db)
|
||||
e2e = [
|
||||
("持有者 `terminated` 的锁**真被解除**", not (T / "lock-dead").exists()),
|
||||
("持有者 `working` 的锁**原封不动**(R9,本用例的变异对照)", (T / "lock-alive").is_dir()),
|
||||
("`OWNER` 里没记 sid 的锁**原封不动**(fail-closed)", (T / "lock-nosid").is_dir()),
|
||||
("有锁未动 ⇒ 退出码 1(⛔ 不是「我全清了」的 0)", rc == 1),
|
||||
("输出里写明了授权来源(可追溯)", "2026-10-09" in out),
|
||||
]
|
||||
|
||||
# 反向对照:宿主库读不到 ⇒ **一把都不许动**
|
||||
_sh.rmtree(T, ignore_errors=True)
|
||||
T.mkdir(parents=True, exist_ok=True)
|
||||
_mk("lock-dead", "执行会话-甲", "deadbeef-0000-0000-0000-000000000001")
|
||||
rc2, out2 = _run(T / "不存在.db")
|
||||
neg = [
|
||||
("宿主库读不到 ⇒ 一把锁都不动(fail-closed)", (T / "lock-dead").is_dir()),
|
||||
("宿主库读不到 ⇒ 退出码 1", rc2 == 1),
|
||||
]
|
||||
_sh.rmtree(T, ignore_errors=True)
|
||||
return unit + e2e + neg
|
||||
|
||||
|
||||
@case("🔴🔴 遇阻碍 ⇒ 置「阻碍」⇒ 机制暂停(⛔ 但「已完成」仍要建 —— 变异对照)")
|
||||
def t_blocked_goal_pauses_check():
|
||||
"""🔴 2026-10-09 立(用户令逐字:「**其他类型 阻碍 遇到阻碍修改目标状态 暂停机制 让用户决策**」)。
|
||||
|
||||
**病根**(`contentm_agent` 实测坐实):口径「遇阻碍 ⇒ 改目标状态 ⇒ 暂停机制」**只在注释里**,
|
||||
代码里没接:① 闸③「`sessions-ended` 队列必须非空」对一条 `blocked` **恒真**;
|
||||
② 闸① 2026-10-04 收窄时把 `sessions-ended` **整条腿**摘了出去 ⇒ 就算置了「阻碍」也照样建;
|
||||
③ 结果检查 prompt 又明写「你没有改目标状态的出口」⇒ 看得见的没权、有权的看不见。
|
||||
⇒ 三条腿互相锁死,只剩**每 30 分钟空转一棒**(实测连开 11 棒、约 5 小时、零派活)。
|
||||
|
||||
⚠️ **变异对照非有不可**:只验「阻碍 ⇒ 不建」的话,把闸① **整道**搬回来(=2026-10-04 修过的
|
||||
病:目标被标「已完成」而队列里还有活 ⇒ 活永远没人接)也照样全绿。⇒ 必须同时验「已完成 ⇒ 仍建」。
|
||||
"""
|
||||
import inspect as _ins
|
||||
m = imp()
|
||||
made = []
|
||||
|
||||
def _fake_create(name, prompt, delay_s=90, cwd=""):
|
||||
made.append(name)
|
||||
return {"ok": True, "id": "selftest", "fire_at": "00:00"}
|
||||
|
||||
# 只桩掉"外部世界",闸本身的判据一律走真代码
|
||||
m.create_check_schedule = _fake_create
|
||||
m._all_sessions_idle = lambda: True
|
||||
m.queue_pending = lambda: 1 # 队列非空 ⇒ 满足闸③(`sessions-ended` 的条件)
|
||||
m.ws_pending_schedules = lambda exclude=None: []
|
||||
m.active_schedule_exists = lambda n: False
|
||||
m._check_stamp = lambda: {}
|
||||
m._check_stamp_save = lambda d: None
|
||||
m.log = lambda *a, **k: None
|
||||
|
||||
out = []
|
||||
for life, want in ((m.GOAL_LIFE_BLOCK, False), (m.GOAL_LIFE_RUN, True),
|
||||
(m.GOAL_LIFE_DONE, True), (m.GOAL_LIFE_WAIT, True)):
|
||||
made.clear()
|
||||
m.goal_life = (lambda L: (lambda: L))(life)
|
||||
try:
|
||||
r = m.maybe_spawn_check_agent("sessions-ended")
|
||||
except Exception as e:
|
||||
out.append(("目标=%s:闸不炸(实得 %s)" % (life, e.__class__.__name__), False))
|
||||
continue
|
||||
built = (r is not None) or bool(made)
|
||||
note = "建" if built else "不建"
|
||||
if life == m.GOAL_LIFE_BLOCK:
|
||||
out.append(("目标=阻碍 ⇒ **停建**(机制暂停、等用户决策;实得:%s)" % note, not built))
|
||||
elif life == m.GOAL_LIFE_DONE:
|
||||
out.append(("目标=已完成 ⇒ **仍要建**(⛔ 别把 2026-10-04 的病搬回来;实得:%s)" % note, built))
|
||||
else:
|
||||
out.append(("目标=%s ⇒ 照常建(实得:%s)" % (life, note), built))
|
||||
|
||||
# 结构断言:闸① 必须**在闸②(会话空闲)之前**读到 `goal_life()`(⛔ 顺序颠倒=白拦)
|
||||
src = _ins.getsource(m.maybe_spawn_check_agent)
|
||||
p1 = src.find("goal_life()")
|
||||
p2 = src.find("_all_sessions_idle()")
|
||||
out.append(("闸① 排在闸② 之前(先判目标状态,再判会话是否结束)", p1 >= 0 and p2 >= 0 and p1 < p2))
|
||||
out.append(("闸① 保留 `queue-empty` 那档原样(⛔ 没把 2026-10-04 的收窄改坏)",
|
||||
'reason == "queue-empty"' in src and "GOAL_LIFE_RUN" in src))
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user