ADR-0013 单一事务边界落地——请求级事务(issue #108 Q1) #151

Open
opened 2026-08-18 02:51:51 +00:00 by KumaAgent · 5 comments
Member

Parent

#108 Q1(issue #108 grill,2026-08-18 拍板:现在就做真正的请求级事务)

What to build

ADR-0013 决策节写的是"core 内部持久写包进单一 DB 事务,崩溃=从未commit=自动回滚",但实测约 44 处
写入点(画像/事件/关系/情感态等)各自独立开 session、各自提交,没有共享请求级事务。把 session 沿
StoragePort 全协议往下传,让同一次触发内的全部持久写共享一个事务。

原文(docs/adr/0013-crash-recovery-and-side-effect-contract.md「决策」节):

core 内部持久写(记忆/画像/情感态/关系/delta)→ 整个请求包进单一 DB 事务;崩溃 = 从未 commit
= 自动回滚。"回退"由此免费拿到,与 fail-closed 对齐。

排除项(ADR-0013 决策节明文豁免,不要动)

Sent Log 写入必须独立于请求事务、随发即持久(不进第①层的回滚事务)。

Acceptance criteria

  • StoragePort 协议方法新增可选的事务/session 传递机制,_debounced_flush/_evaluate_drive_tick_chat
    等触发入口在一次触发开始时开一个 session,贯穿传给该次触发内的全部持久写调用
  • 全仓约 44 处独立 await s.commit() 调用点(画像/事件/知识/关系轴/情感态等,storage.py)改为
    复用传入的 session,不再各自开 session 各自提交
  • Sent Log/Outbox 写入不受影响,继续独立提交、随发即持久——这是 ADR-0013 明文豁免的第③层,
    不要并进请求级事务
  • APScheduler 定时作业(当前 5 个:entry_callback.py×1、entry_drive_tick.py×1、
    entry_offline_jobs.py×3)各自的写入范围需要显式判断:是否也该在各自的作业执行范围内共享一个
    事务,还是维持独立提交——写进本票的实现说明,不要含糊带过
  • 全量测试通过,且补一条端到端测试:模拟一次触发中途抛异常,断言该次触发内全部持久写都没
    有落库(不是只测其中一处)

Not in scope

  • 不碰 Sent Log/Outbox 的提交语义
  • 不新增崩溃恢复相关的新机制——这是把 ADR-0013 早就拍板的设计做实,不是新决策
## Parent #108 Q1(issue #108 grill,2026-08-18 拍板:现在就做真正的请求级事务) ## What to build ADR-0013 决策节写的是"core 内部持久写包进单一 DB 事务,崩溃=从未commit=自动回滚",但实测约 44 处 写入点(画像/事件/关系/情感态等)各自独立开 session、各自提交,没有共享请求级事务。把 session 沿 `StoragePort` 全协议往下传,让同一次触发内的全部持久写共享一个事务。 原文(`docs/adr/0013-crash-recovery-and-side-effect-contract.md`「决策」节): > core 内部持久写(记忆/画像/情感态/关系/delta)→ 整个请求包进**单一 DB 事务**;崩溃 = 从未 commit > = 自动回滚。"回退"由此免费拿到,与 fail-closed 对齐。 **排除项(ADR-0013 决策节明文豁免,不要动)**: > Sent Log 写入必须独立于请求事务、随发即持久(不进第①层的回滚事务)。 ## Acceptance criteria - [ ] `StoragePort` 协议方法新增可选的事务/session 传递机制,`_debounced_flush`/`_evaluate_drive_tick_chat` 等触发入口在一次触发开始时开一个 session,贯穿传给该次触发内的全部持久写调用 - [ ] 全仓约 44 处独立 `await s.commit()` 调用点(画像/事件/知识/关系轴/情感态等,`storage.py`)改为 复用传入的 session,不再各自开 session 各自提交 - [ ] **Sent Log/Outbox 写入不受影响**,继续独立提交、随发即持久——这是 ADR-0013 明文豁免的第③层, 不要并进请求级事务 - [ ] APScheduler 定时作业(当前 5 个:`entry_callback.py`×1、`entry_drive_tick.py`×1、 `entry_offline_jobs.py`×3)各自的写入范围需要显式判断:是否也该在各自的作业执行范围内共享一个 事务,还是维持独立提交——写进本票的实现说明,不要含糊带过 - [ ] 全量测试通过,且补一条端到端测试:模拟一次触发中途抛异常,断言该次触发内**全部**持久写都没 有落库(不是只测其中一处) ## Not in scope - 不碰 Sent Log/Outbox 的提交语义 - 不新增崩溃恢复相关的新机制——这是把 ADR-0013 早就拍板的设计做实,不是新决策
Author
Member

Understand + Design-Verify 阶段发现记录

在开始 TDD 实现前,先做了 Understand 阶段(核实 storage.py 写入点全貌 + StoragePort 协议面 + 5 个 APScheduler 作业的写入范围),再针对据此提出的一版实现方案(session 可选参数穿透 + 事务边界按"不可逆副作用前后拆段"切分)跑了一轮对抗式 design-verify(2 个角度并行审查产出 10 条候选发现,每条再各自独立二次对抗复核)。

结论:9/10 条发现被复核确认成立(含 4 条 HIGH),1 条被推翻(但推翻它的复核本身也订正了一个数字错误)。真实改动面远超本票 AC 字面描述的"约44处独立commit调用点",记录如下供后续拆票参考。

已确认发现(9条)

[HIGH] 机制存在致命实现陷阱:字面实现会导致数据静默丢失
storage.py 现有44处写入结构均为 session = get_scoped_session(); async with session() as s: ...; await s.commit()。若只把 commit() 换成 flush()、不去掉外层 async with 包装,async with 退出时仍会无条件 close()——已用本仓真实 SQLAlchemy + aiosqlite 实测复现:flush 过的数据在外层最终 commit 之后依然不在库里,且全程不抛任何异常,不会被现有测试捕获。落地时必须明确:接收到外部传入 session 的分支绝不能再用 async with 包装它,只能直接操作调用方传入的对象。

[HIGH] 即使写方法改对,合并事务窗口内的"读方法"仍会打断事务
get_scoped_session() 按 (event, matcher) 缓存同一个 Session 对象没错,但每个方法各自的 async with session() as s: 退出都会 close() 一次——44个写方法之外的"读方法"目前全部保持独立开关,如果它们在合并事务窗口内被调用,会把前面已 flush 未 commit 的写入静默回滚。已在方案自己点名要合并的三处目标里找到真实实例:

  • entry_callback.py::_deliver_callbackdelete_callback(写) → get_fast_affective_state(读,未改造) → _write_decision_snapshot(写)——中间这次读会导致 delete_callback 被静默回滚,下一轮 _callback_scan 重新判定为待定,造成 Callback 重复送出。
  • delta.py::_compress_one_chatwrite_tentative_facts/write_event/write_knowledge 之后,循环里先 get_relationship_edges(读) 再判断写——会把前面已 flush 的写入回滚,且不触发方案自己设计的"失败回退到 Delta 缓冲"兜底。
  • reflection.py::_apply_outcomeupdate_fast_affective_state/update_slow_affective_state 之后 get_proactivity_offset(读) 再写——同样会被打断。

[HIGH] session 从触发入口传到 storage.py 的路径缺失
中间还隔着 gate_pipeline.py(_evaluate_unified_gate/_write_decision_snapshot)、gating.py(evaluate_gate) 等中转函数,以及 delta.py/reflection.py/drift_veto.py/unanswered_escalation.py/proactive_reconnect.py 等模块里同样承担"触发入口→storage调用"中转职责的函数——这些函数目前都不接受 session 参数,若不改,storage.py 新增的可选 session 参数永远只会拿到默认 None,AC 要求的"贯穿传给该次触发内的全部持久写调用"无法达成。另外核实了一条可能的捷径不可行:ArisePorts.storage 是跨并发请求的固定单例(不是工厂),不能在触发开始时替换成"绑定本次 session 的适配器",否则会和其它并发到达的 chat/event 互相踩踏。

[HIGH] 5个 APScheduler 作业的 session 隔离机制在设计里完全缺失
_callback_scan/_drive_tick/_compress_pending_delta/_run_reflection/_run_unanswered_escalation 全部是 @scheduler.scheduled_job 直接装饰的顶层协程,跑在 nonebot 事件/matcher 上下文之外——get_scoped_session() 的 scope key (id(current_event.get(None)), current_matcher.get(None)) 对这 5 个作业全部坍缩成同一个 (id(None), None),即它们实际共享同一个 Python Session 对象。现状因为每次调用都是"开-用-commit-close"的极短窗口所以风险很小;但方案要把持有窗口拉长到跨越真实 LLM 调用的整个 item 处理过程(秒级)。APScheduler 默认配置不会阻止 5 个不同 job 之间的并发重叠执行——一旦重叠,会在同一个不支持并发使用的 AsyncSession 对象上互相踩踏,存在真实的会话状态损坏风险。这 5 个作业需要一套完全独立于 nonebot scoped-session 的会话机制,不能直接套用事件驱动入口那一套。

[MEDIUM] "不可逆副作用前后拆两段"字面上收窄了 ADR-0013 的范围,未被记录为 ADR 更新
ADR-0013 决策原文把请求内副作用分三层,"①core内部持久写 → 整个请求包进单一DB事务",明文豁免的只有第③层"已发射到平台的动作"和 Sent Log。silence_budget/决策快照/reconnect_cooldown 等写入按字面都属于第①层,理应在"整个请求"范围内——而方案把 proceed 分支的 silence_budget 写入摘出来、改成 LLM 调用前独立提交、不参与共享事务(工程理由合理:真实发送已不可逆,事后写入失败不该连带撤销已发生的预算消费),但这字面上是对 ADR-0013 §1 范围的收窄,需要走一次 ADR 更新记录,而不只是体现在实现方案的散文里。

[MEDIUM] 遗漏了 2 个结构相同的触发入口
_evaluate_unified_gate 实际有 4 个调用点(反应式、Drive Tick、环境信号、即刻追问),方案设计段落只讨论了反应式和 Drive Tick 两个。_run_group_followup_check(群聊即刻追问)和 _process_environment_signal(环境信号)具有完全相同的"门控前写→拦截或放行→真实LLM/发送→门控后写"结构,需要同样纳入设计范围(_run_group_followup_check 因为是 asyncio.create_task 发起的脱钩后台任务,还需要额外考虑"不能参与父调用请求级事务,需独立开自己的session")。

[MEDIUM] _write_decision_snapshot 吞异常在共享事务下会变成"伪装成功"陷阱
该函数内部 try: await ports.storage.write_decision_snapshot(...) except Exception: logger.exception(...),只记日志不重新抛出——这个设计在"快照写入本身失败"场景下合理,但一旦这次写入参与共享事务,前面某条语句已经失败导致事务处于中止状态时,这次写入会把"事务已中止"的错误当成"这次快照写坏了"悄悄吞掉,函数正常返回,日志仍显示决策成功,而同一事务里更早的、本该一起提交的写入也全部没了,且没有任何异常线索指向真实原因。设计需要给出应对(例如共享事务场景下改为失败即重新抛出,或把这次写入排除在共享事务之外)。

[MEDIUM] record_pool_usage 现有的 rollback 降级逻辑与共享 session 冲突
该方法 UPDATE 命中0行时尝试 INSERT,撞主键触发 IntegrityError 后显式 await s.rollback() 再退回 UPDATE 重试——这是应对并发写同一 (pool, day) 行的既有安全设计,不是可删的模板代码。若这次写入的 session 被外层共享,rollback() 会撤销该 session 自上次 commit 以来的全部未提交更改,不只是它自己的 INSERT 尝试,会连带撤销同一共享事务内其它已 flush 的兄弟写入(例如 delta 压缩批里更早产出的画像候选/事件)。需要改用 SAVEPOINT(session.begin_nested())等手段把这次 rollback 的影响范围收窄到它自己的语句。

[MEDIUM] SendFailureModel 的"纳入常规改造"裁定值得重新考虑
record_send_failure/list_send_failures/clear_send_failures 三个方法的唯一调用点全部深埋在 RuntimeLoop.run()/run_light()/_run_directive_turn() 内部(_flush() 的发送失败分支,以及 run() 方法体首尾),与被 AC 明文排除的 Sent Log record()/Outbox clear_outbox() 紧邻同一段代码、同属"真实发送已跑完/正在跑"这个不可逆区间。既然这个区间结构性地不参与共享请求事务,"纳入常规改造"这个裁定在实践中不会引入 bug,但也不会有任何实际效果(只是死代码),应该像 Sent Log/Outbox 一样明确排除,而不是保留一个不产生效果的"纳入"标注。

被推翻的发现(1条,附带一次数字订正)

有一条发现指出 Understand 阶段"storage.py 恰好44个方法"的计数应订正为43个(ensure_schema/recover 是模块级函数不应计入 PersistentStorage 类方法)——复核时用 AST 独立重新统计,确认这条发现指出的问题方向是对的,但它自己给出的订正数字"43"同样有误:真实数字应为 42search_learned_stickers 是纯 Qdrant 向量检索,不含 get_scoped_session/commit,被错误计入了43这个数字里)。SendFailureModel 的两个写方法本来就已经在这42个之内,这个计数订正不影响其它任何裁定。

对范围的影响

真实改动面不只是 storage.py 的 ~42 个写方法 + SentLog 镜像,还需要:

  • 贯穿 gate_pipeline.py/gating.py/delta.py/reflection.py/drift_veto.py/unanswered_escalation.py/proactive_reconnect.py 等中间层模块
  • 对 4 个触发入口(不是 2 个)做 pre/post-LLM 拆分
  • 为 5 个 APScheduler 作业设计一套完全独立于 nonebot scoped-session 的会话机制
  • 至少 3 处已识别的"写后读打断"位置需要把对应读方法也纳入 session 参数机制
  • record_pool_usage 需要 SAVEPOINT 特殊处理
  • 一次 ADR-0013 更新记录事务边界的范围收窄
  • SendFailureModel 排除范围的最终裁定

这个规模已经超出单个 PR 合理的评审/合并粒度,建议在开始 TDD 前先按逻辑边界重新拆分子任务。

## Understand + Design-Verify 阶段发现记录 在开始 TDD 实现前,先做了 Understand 阶段(核实 storage.py 写入点全貌 + StoragePort 协议面 + 5 个 APScheduler 作业的写入范围),再针对据此提出的一版实现方案(session 可选参数穿透 + 事务边界按"不可逆副作用前后拆段"切分)跑了一轮对抗式 design-verify(2 个角度并行审查产出 10 条候选发现,每条再各自独立二次对抗复核)。 **结论:9/10 条发现被复核确认成立(含 4 条 HIGH),1 条被推翻(但推翻它的复核本身也订正了一个数字错误)。真实改动面远超本票 AC 字面描述的"约44处独立commit调用点",记录如下供后续拆票参考。** ### 已确认发现(9条) **[HIGH] 机制存在致命实现陷阱:字面实现会导致数据静默丢失** storage.py 现有44处写入结构均为 `session = get_scoped_session(); async with session() as s: ...; await s.commit()`。若只把 `commit()` 换成 `flush()`、不去掉外层 `async with` 包装,`async with` 退出时仍会无条件 `close()`——已用本仓真实 SQLAlchemy + aiosqlite 实测复现:flush 过的数据在外层最终 commit 之后依然不在库里,且全程不抛任何异常,不会被现有测试捕获。落地时必须明确:接收到外部传入 session 的分支绝不能再用 `async with` 包装它,只能直接操作调用方传入的对象。 **[HIGH] 即使写方法改对,合并事务窗口内的"读方法"仍会打断事务** get_scoped_session() 按 (event, matcher) 缓存同一个 Session 对象没错,但每个方法各自的 `async with session() as s:` 退出都会 close() 一次——44个写方法之外的"读方法"目前全部保持独立开关,如果它们在合并事务窗口内被调用,会把前面已 flush 未 commit 的写入静默回滚。已在方案自己点名要合并的三处目标里找到真实实例: - `entry_callback.py::_deliver_callback`:`delete_callback`(写) → `get_fast_affective_state`(读,未改造) → `_write_decision_snapshot`(写)——中间这次读会导致 delete_callback 被静默回滚,下一轮 `_callback_scan` 重新判定为待定,造成 Callback 重复送出。 - `delta.py::_compress_one_chat`:`write_tentative_facts`/`write_event`/`write_knowledge` 之后,循环里先 `get_relationship_edges`(读) 再判断写——会把前面已 flush 的写入回滚,且不触发方案自己设计的"失败回退到 Delta 缓冲"兜底。 - `reflection.py::_apply_outcome`:`update_fast_affective_state`/`update_slow_affective_state` 之后 `get_proactivity_offset`(读) 再写——同样会被打断。 **[HIGH] session 从触发入口传到 storage.py 的路径缺失** 中间还隔着 `gate_pipeline.py`(`_evaluate_unified_gate`/`_write_decision_snapshot`)、`gating.py`(`evaluate_gate`) 等中转函数,以及 `delta.py`/`reflection.py`/`drift_veto.py`/`unanswered_escalation.py`/`proactive_reconnect.py` 等模块里同样承担"触发入口→storage调用"中转职责的函数——这些函数目前都不接受 session 参数,若不改,storage.py 新增的可选 session 参数永远只会拿到默认 None,AC 要求的"贯穿传给该次触发内的全部持久写调用"无法达成。另外核实了一条可能的捷径不可行:`ArisePorts.storage` 是跨并发请求的固定单例(不是工厂),不能在触发开始时替换成"绑定本次 session 的适配器",否则会和其它并发到达的 chat/event 互相踩踏。 **[HIGH] 5个 APScheduler 作业的 session 隔离机制在设计里完全缺失** `_callback_scan`/`_drive_tick`/`_compress_pending_delta`/`_run_reflection`/`_run_unanswered_escalation` 全部是 `@scheduler.scheduled_job` 直接装饰的顶层协程,跑在 nonebot 事件/matcher 上下文之外——`get_scoped_session()` 的 scope key `(id(current_event.get(None)), current_matcher.get(None))` 对这 5 个作业全部坍缩成同一个 `(id(None), None)`,即它们实际共享同一个 Python Session 对象。现状因为每次调用都是"开-用-commit-close"的极短窗口所以风险很小;但方案要把持有窗口拉长到跨越真实 LLM 调用的整个 item 处理过程(秒级)。APScheduler 默认配置不会阻止 5 个不同 job 之间的并发重叠执行——一旦重叠,会在同一个不支持并发使用的 AsyncSession 对象上互相踩踏,存在真实的会话状态损坏风险。这 5 个作业需要一套完全独立于 nonebot scoped-session 的会话机制,不能直接套用事件驱动入口那一套。 **[MEDIUM] "不可逆副作用前后拆两段"字面上收窄了 ADR-0013 的范围,未被记录为 ADR 更新** ADR-0013 决策原文把请求内副作用分三层,"①core内部持久写 → 整个请求包进单一DB事务",明文豁免的只有第③层"已发射到平台的动作"和 Sent Log。silence_budget/决策快照/reconnect_cooldown 等写入按字面都属于第①层,理应在"整个请求"范围内——而方案把 proceed 分支的 silence_budget 写入摘出来、改成 LLM 调用前独立提交、不参与共享事务(工程理由合理:真实发送已不可逆,事后写入失败不该连带撤销已发生的预算消费),但这字面上是对 ADR-0013 §1 范围的收窄,需要走一次 ADR 更新记录,而不只是体现在实现方案的散文里。 **[MEDIUM] 遗漏了 2 个结构相同的触发入口** `_evaluate_unified_gate` 实际有 4 个调用点(反应式、Drive Tick、环境信号、即刻追问),方案设计段落只讨论了反应式和 Drive Tick 两个。`_run_group_followup_check`(群聊即刻追问)和 `_process_environment_signal`(环境信号)具有完全相同的"门控前写→拦截或放行→真实LLM/发送→门控后写"结构,需要同样纳入设计范围(`_run_group_followup_check` 因为是 `asyncio.create_task` 发起的脱钩后台任务,还需要额外考虑"不能参与父调用请求级事务,需独立开自己的session")。 **[MEDIUM] `_write_decision_snapshot` 吞异常在共享事务下会变成"伪装成功"陷阱** 该函数内部 `try: await ports.storage.write_decision_snapshot(...) except Exception: logger.exception(...)`,只记日志不重新抛出——这个设计在"快照写入本身失败"场景下合理,但一旦这次写入参与共享事务,前面某条语句已经失败导致事务处于中止状态时,这次写入会把"事务已中止"的错误当成"这次快照写坏了"悄悄吞掉,函数正常返回,日志仍显示决策成功,而同一事务里更早的、本该一起提交的写入也全部没了,且没有任何异常线索指向真实原因。设计需要给出应对(例如共享事务场景下改为失败即重新抛出,或把这次写入排除在共享事务之外)。 **[MEDIUM] `record_pool_usage` 现有的 rollback 降级逻辑与共享 session 冲突** 该方法 UPDATE 命中0行时尝试 INSERT,撞主键触发 IntegrityError 后显式 `await s.rollback()` 再退回 UPDATE 重试——这是应对并发写同一 (pool, day) 行的既有安全设计,不是可删的模板代码。若这次写入的 session 被外层共享,`rollback()` 会撤销该 session 自上次 commit 以来的全部未提交更改,不只是它自己的 INSERT 尝试,会连带撤销同一共享事务内其它已 flush 的兄弟写入(例如 delta 压缩批里更早产出的画像候选/事件)。需要改用 SAVEPOINT(`session.begin_nested()`)等手段把这次 rollback 的影响范围收窄到它自己的语句。 **[MEDIUM] SendFailureModel 的"纳入常规改造"裁定值得重新考虑** `record_send_failure`/`list_send_failures`/`clear_send_failures` 三个方法的唯一调用点全部深埋在 `RuntimeLoop.run()`/`run_light()`/`_run_directive_turn()` 内部(`_flush()` 的发送失败分支,以及 `run()` 方法体首尾),与被 AC 明文排除的 Sent Log `record()`/Outbox `clear_outbox()` 紧邻同一段代码、同属"真实发送已跑完/正在跑"这个不可逆区间。既然这个区间结构性地不参与共享请求事务,"纳入常规改造"这个裁定在实践中不会引入 bug,但也不会有任何实际效果(只是死代码),应该像 Sent Log/Outbox 一样明确排除,而不是保留一个不产生效果的"纳入"标注。 ### 被推翻的发现(1条,附带一次数字订正) 有一条发现指出 Understand 阶段"storage.py 恰好44个方法"的计数应订正为43个(`ensure_schema`/`recover` 是模块级函数不应计入 PersistentStorage 类方法)——复核时用 AST 独立重新统计,确认这条发现指出的问题方向是对的,但它自己给出的订正数字"43"同样有误:真实数字应为 **42**(`search_learned_stickers` 是纯 Qdrant 向量检索,不含 `get_scoped_session`/`commit`,被错误计入了43这个数字里)。SendFailureModel 的两个写方法本来就已经在这42个之内,这个计数订正不影响其它任何裁定。 ### 对范围的影响 真实改动面不只是 storage.py 的 ~42 个写方法 + SentLog 镜像,还需要: - 贯穿 `gate_pipeline.py`/`gating.py`/`delta.py`/`reflection.py`/`drift_veto.py`/`unanswered_escalation.py`/`proactive_reconnect.py` 等中间层模块 - 对 4 个触发入口(不是 2 个)做 pre/post-LLM 拆分 - 为 5 个 APScheduler 作业设计一套完全独立于 nonebot scoped-session 的会话机制 - 至少 3 处已识别的"写后读打断"位置需要把对应读方法也纳入 session 参数机制 - `record_pool_usage` 需要 SAVEPOINT 特殊处理 - 一次 ADR-0013 更新记录事务边界的范围收窄 - SendFailureModel 排除范围的最终裁定 这个规模已经超出单个 PR 合理的评审/合并粒度,建议在开始 TDD 前先按逻辑边界重新拆分子任务。
Author
Member

评估侧已处理上面 Design-Verify 发现里 ADR 层面的那一条:silence_budget/决策快照/reconnect 冷却
写入独立于请求事务这个范围收窄,已补进 ADR-0013 更新节(PR #160)。

其余 9 条(含 4 条 HIGH)都是纯技术实现范围——async with 误用陷阱、写后读打断事务、session
传递路径缺失中间层、5 个 APScheduler 作业的隔离机制缺失、_write_decision_snapshot 吞异常陷阱、
record_pool_usage 的 SAVEPOINT 需求、SendFailureModel 范围裁定、遗漏的 2 个触发入口、44→42
数字订正——这些不涉及 ADR/产品决策,原样保留在上面评论里,供拆票时逐条对应到子票 AC,不要在
转手过程中丢失。

按实现侧自己的结论"规模超出单个 PR 合理粒度",状态位改为「待拆片」,转交工单侧按逻辑边界拆分
子任务。

评估侧已处理上面 Design-Verify 发现里 ADR 层面的那一条:silence_budget/决策快照/reconnect 冷却 写入独立于请求事务这个范围收窄,已补进 ADR-0013 更新节(PR #160)。 其余 9 条(含 4 条 HIGH)都是纯技术实现范围——`async with` 误用陷阱、写后读打断事务、session 传递路径缺失中间层、5 个 APScheduler 作业的隔离机制缺失、`_write_decision_snapshot` 吞异常陷阱、 `record_pool_usage` 的 SAVEPOINT 需求、SendFailureModel 范围裁定、遗漏的 2 个触发入口、44→42 数字订正——这些不涉及 ADR/产品决策,原样保留在上面评论里,供拆票时逐条对应到子票 AC,不要在 转手过程中丢失。 按实现侧自己的结论"规模超出单个 PR 合理粒度",状态位改为「待拆片」,转交工单侧按逻辑边界拆分 子任务。
Author
Member

全 ADR 查漏第二轮(PR #174)给 ADR-0013 补了一条如实标注现状的更新节:「单一 DB 事务」这个①层核心机制本身尚未实现(不只是范围定义不精确),design.md「决策基线」表也一并加了标注。没有开新票——本票就是这条落差的归宿,拆片时这条更新节可以直接引用。

全 ADR 查漏第二轮(PR #174)给 ADR-0013 补了一条如实标注现状的更新节:「单一 DB 事务」这个①层核心机制本身尚未实现(不只是范围定义不精确),design.md「决策基线」表也一并加了标注。没有开新票——本票就是这条落差的归宿,拆片时这条更新节可以直接引用。
Author
Member

全量ADR设计合理性对抗审查发现(2026-08-21):单DB事务目标机制本身与多轮工具循环架构存在结构性冲突

评估侧对全部32条ADR做了一轮设计合理性对抗审查(不是文档-代码一致性核查,是质疑决策本身合不合理),其中一条独立agent对抗式反驳后仍确认成立的发现,与本票直接相关,记录如下供拆片/实现时一并考虑:

发现:ADR-0013①层"整个请求包进单一DB事务"这一目标机制,与本项目自己的多轮工具循环架构存在结构性冲突——要把一次请求内全部持久写纳入同一个事务,就必须让这个事务跨越多次外部LLM API往返(以及可能的host工具调用)持续存活。这是数据库使用上公认的反模式(长时间持有事务横跨慢速外部I/O,易致连接池耗尽/长事务锁争用)。

这与上面Design-Verify阶段已确认的9条发现(尤其"5个APScheduler作业的session隔离机制缺失""写后读打断事务"两条)指向同一类风险的更根本成因:之所以会有这么多session/事务边界的实现陷阱,根源可能就是"整个请求共享一个事务"这个目标本身,在有外部LLM调用穿插的架构下就不是一个健康的设计目标。

建议:拆片时评估是否要把事务边界重新设计为"只包住真正连续的DB写入片段,LLM/工具调用发生在事务之外"(而不是让一个事务横跨整个触发处理过程),而不是把当前"整个请求单一事务"这个目标原样实现出来。这可能意味着 ADR-0013①层的"崩溃=自动回滚"这个免费保证需要重新论证——如果事务本来就不包住LLM调用期间的状态,"崩溃恢复"的实际保证范围会比决策节原文描述的更窄。

这条发现建议在拆片时一并纳入设计考虑,不需要因此新开票——本票(及其"待拆片"状态)就是这条落差的归宿。

## 全量ADR设计合理性对抗审查发现(2026-08-21):单DB事务目标机制本身与多轮工具循环架构存在结构性冲突 评估侧对全部32条ADR做了一轮设计合理性对抗审查(不是文档-代码一致性核查,是质疑决策本身合不合理),其中一条独立agent对抗式反驳后仍确认成立的发现,与本票直接相关,记录如下供拆片/实现时一并考虑: **发现**:ADR-0013①层"整个请求包进单一DB事务"这一目标机制,与本项目自己的多轮工具循环架构存在结构性冲突——要把一次请求内全部持久写纳入同一个事务,就必须让这个事务跨越多次外部LLM API往返(以及可能的host工具调用)持续存活。这是数据库使用上公认的反模式(长时间持有事务横跨慢速外部I/O,易致连接池耗尽/长事务锁争用)。 这与上面Design-Verify阶段已确认的9条发现(尤其"5个APScheduler作业的session隔离机制缺失""写后读打断事务"两条)指向同一类风险的更根本成因:之所以会有这么多session/事务边界的实现陷阱,根源可能就是"整个请求共享一个事务"这个目标本身,在有外部LLM调用穿插的架构下就不是一个健康的设计目标。 **建议**:拆片时评估是否要把事务边界重新设计为"只包住真正连续的DB写入片段,LLM/工具调用发生在事务之外"(而不是让一个事务横跨整个触发处理过程),而不是把当前"整个请求单一事务"这个目标原样实现出来。这可能意味着 ADR-0013①层的"崩溃=自动回滚"这个免费保证需要重新论证——如果事务本来就不包住LLM调用期间的状态,"崩溃恢复"的实际保证范围会比决策节原文描述的更窄。 这条发现建议在拆片时一并纳入设计考虑,不需要因此新开票——本票(及其"待拆片"状态)就是这条落差的归宿。
Author
Member

拆片前的事务边界设计方向已与用户确认,传递给评估侧正式记录

2026-08-21 那条"全量ADR设计合理性对抗审查"发现(①层目标机制与多轮工具循环架构结构性冲突)明确写着"拆片时评估是否要重新设计事务边界……不需要因此新开票,本票就是这条落差的归宿"——工单侧拆片前就这个分岔跟用户确认了方向,结论如下,供评估侧正式写进 ADR-0013 更新节(本片不代为写这段文档,按分工文档由评估侧维护):

已确认方向:事务边界改为"分段",不是原字面的"整个请求单一长事务"。

具体口径:

  • 事务只包住连续无外部 I/O 打断的一段纯 DB 读写;一旦下一步要发起任何外部调用(LLM 补全、host 工具调用),当前事务必须先提交/关闭,下一段写入重新开一个新事务。
  • 不是严格"一轮 while True 工具循环一个事务",是严格按"两次外部 I/O 之间"切分——如果某一轮工具循环内又调用了一次外部 host 工具,这次工具执行前也要先把当前事务收口。
  • 这意味着"崩溃=自动回滚整个请求"这条原始保证收窄为**"崩溃=自动回滚当前未提交的那一段,更早已提交的段落不会被追溯撤销"**。

选择这个方向而非按原字面实现的理由:design-verify 阶段 + 两轮 ADR 查漏 + 一轮全量设计合理性对抗审查,三条独立线索都指向同一个根因——把事务持有窗口拉长到跨越外部 LLM 调用,是数据库使用上公认的反模式(长事务横跨慢速外部 I/O,易致连接池耗尽/长事务锁争用),也是本票 design-verify 阶段那一批"写后读打断事务""5个APScheduler作业session collapse"等具体实现陷阱的更根本成因。按原字面"单一长事务"实现,等于把已经识别出的反模式原样做实。

后续 5 片 slice ticket(存储层事务基建 / 四个门控触发入口 / Delta压缩周期 / 反思闭环家族 / APScheduler定时作业)均按这个方向设计,AC 里会引用这条评论。

## 拆片前的事务边界设计方向已与用户确认,传递给评估侧正式记录 2026-08-21 那条"全量ADR设计合理性对抗审查"发现(①层目标机制与多轮工具循环架构结构性冲突)明确写着"拆片时评估是否要重新设计事务边界……不需要因此新开票,本票就是这条落差的归宿"——工单侧拆片前就这个分岔跟用户确认了方向,结论如下,供评估侧正式写进 ADR-0013 更新节(本片不代为写这段文档,按分工文档由评估侧维护): **已确认方向:事务边界改为"分段",不是原字面的"整个请求单一长事务"。** 具体口径: - 事务只包住**连续无外部 I/O 打断**的一段纯 DB 读写;一旦下一步要发起任何外部调用(LLM 补全、host 工具调用),当前事务必须先提交/关闭,下一段写入重新开一个新事务。 - 不是严格"一轮 `while True` 工具循环一个事务",是严格按"两次外部 I/O 之间"切分——如果某一轮工具循环内又调用了一次外部 host 工具,这次工具执行前也要先把当前事务收口。 - 这意味着"崩溃=自动回滚整个请求"这条原始保证收窄为**"崩溃=自动回滚当前未提交的那一段,更早已提交的段落不会被追溯撤销"**。 选择这个方向而非按原字面实现的理由:design-verify 阶段 + 两轮 ADR 查漏 + 一轮全量设计合理性对抗审查,三条独立线索都指向同一个根因——把事务持有窗口拉长到跨越外部 LLM 调用,是数据库使用上公认的反模式(长事务横跨慢速外部 I/O,易致连接池耗尽/长事务锁争用),也是本票 design-verify 阶段那一批"写后读打断事务""5个APScheduler作业session collapse"等具体实现陷阱的更根本成因。按原字面"单一长事务"实现,等于把已经识别出的反模式原样做实。 后续 5 片 slice ticket(存储层事务基建 / 四个门控触发入口 / Delta压缩周期 / 反思闭环家族 / APScheduler定时作业)均按这个方向设计,AC 里会引用这条评论。
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ProjectKuma/arise#151
No description provided.