四个门控触发入口 + RuntimeLoop主循环的事务分段接线 #198

Open
opened 2026-08-21 06:13:06 +00:00 by KumaAgent · 0 comments
Member

Parent

#151(ADR-0013 ①层单一事务边界落地,issue #108 Q1;状态"待拆片")——本片是拆分后 5 片之一,
负责其中「四个门控触发入口」:反应式 _debounced_flush、Drive Tick _evaluate_drive_tick_chat
群聊即刻追问 _run_group_followup_check、环境信号 _process_environment_signal,这四个都经由
共享的 _evaluate_unified_gategate_pipeline.py)/evaluate_gategating.py)机制。

事务边界方向已改:#151 comments"拆片前的事务边界设计方向已与用户确认"(2026-08-21)——
不是原 ADR-0013 字面"整个请求单一长事务",而是分段

事务只包住连续无外部 I/O 打断的一段纯 DB 读写;一旦下一步要发起任何外部调用(LLM 补全、host
工具调用),当前事务必须先提交/关闭,下一段写入重新开一个新事务……不是严格"一轮 while True
工具循环一个事务",是严格按"两次外部 I/O 之间"切分……崩溃=自动回滚当前未提交的那一段
更早已提交的段落不会被追溯撤销。

本票把这个方向落实成四个入口各自的分段边界,以及它们共同调用的 RuntimeLoop 主循环
run/run_light/_run_tool_loop 等)内部的分段
(四个入口调用的 loop.run*() 不是无副作用
黑盒,其内部的多轮工具循环与前后写入同样需要按分段设计处理,见"八"节;四个入口是它唯一的调用方,
不适合另开一片),并接好地基片交付的 session/segment API(不重新设计该 API 本身)。

核实中发现代码库自 design-verify 阶段以来又新增了两个结构相同的触发入口
_handle_group_slow_interrupt/issue #159/ADR-0032、_process_ambient_telemetry_signal/issue #153),
本票会改动它们共用的 _evaluate_unified_gate/apply_tier1_ambient_contagion 等函数签名,但不改
这两处调用点本身
——它们的事务分段接线开了独立的后续票(依赖本票),见 Blocked by 同批次的姊妹票。

What to build

四个入口目前各自内部按"读→算→写"顺序调用 storage.py 方法,每个方法各自 get_scoped_session()+
async with+commit(),互不共享事务,也没有任何函数把 session 接力传下去。要落实分段设计,需要:

  1. gate_pipeline.py/gating.py 的中转函数接受 session 参数并往下传(这两个模块被四个入口共同
    调用,是真正的"贯穿"关卡)。
  2. 四个入口各自按"两次外部 I/O 之间"重新划分自己的写入序列为若干段,pre-LLM 段在发起
    loop.run*() 前提交/关闭,post-LLM 段在 loop.run*() 返回后重新开一个。
  3. 三类 ADR-0013 明文排除在"共享分段"之外的决策记账写入(沉默预算消费/决策快照/reconnect 冷却
    记录)改用地基片提供的"独立提交"能力,不使用"并入当前分段"能力——这个区分同时解决了
    design-verify 阶段两条独立发现(entry_callback 写后读打断 + _write_decision_snapshot 吞异常)。
  4. _run_group_followup_check 作为 asyncio.create_task 发起的脱钩后台任务,在自己的函数入口
    独立开一个新 session,不尝试接住父调用(_debounced_flush)已经关闭的段。

核实说明:origin/main 现状与 design-verify 阶段发现描述的代码一致——四处 _evaluate_unified_gate
调用点(entry_reactive.py:698/:832entry_drive_tick.py:393environment_pipeline.py:179)、
entry_callback.py::_deliver_callbackdelete_callback(100行)→get_fast_affective_state(107行)→
_write_decision_snapshot(108行) 序列、_write_decision_snapshot 的吞异常 except Exception: logger.exception(...)(不重新抛出)均逐行核对无误,未发现代码漂移。gate_pipeline.py 模块
文档字符串(现状)显示范围已经比 design-verify 阶段发现时更大
——见下方"范围边界"一节。

Acceptance criteria

一、共享机制:session 贯穿 gate_pipeline.py / gating.py

  • _evaluate_unified_gategate_pipeline.py:155)新增可选 session 参数,签名/复用方式与
    地基片交付的 session 传递 API 一致(不在本片另造一套调用约定);内部往下传给
    get_effective_fast_state/compute_unlock_inputs/get_proactivity_offset/evaluate_gate
  • evaluate_gategating.py:255)同样新增可选 session 参数,往下传给 storage.get_silence_budget
  • apply_tier1_ambient_contagionenvironment_pipeline.py:233_process_environment_signal
    内部调用)新增可选 session 参数,往下传给它内部的两次读(get_fast_affective_state/
    get_slow_affective_state)与两次写(update_fast_affective_state/update_slow_affective_state

出处:#151 comments Design-Verify 阶段 [HIGH] 发现"session 从触发入口传到 storage.py 的路径
缺失"——原文点名 gate_pipeline.py_evaluate_unified_gate/_write_decision_snapshot)、
gating.py 等中转函数"目前都不接受 session 参数,若不改,storage.py 新增的可选 session 参数
永远只会拿到默认 None"。_write_decision_snapshot 的处理方式见本票"七"节(独立提交,不接收
调用方传入的共享段 session)。

二、反应式 _debounced_flushentry_reactive.py:600)分段

  • pre-LLM 段:update_chat_activity(群聊,695行)+ _evaluate_unified_gate 内部除沉默预算
    写入外的读取,合并进同一段;verdict≠proceed 的早退分支到此为止(决策快照见"七"节,独立提交,
    不算这一段)
  • pre-LLM 段在 loop.run(chat_id, messages)(753行)调用前提交/关闭
  • post-LLM 段:set_recall_timing_override/_schedule_group_followup_check/
    record_unanswered_escalation(756-769行)合并进同一段,loop.run() 返回后新开
  • read_delay_window(675行)等待期间不持有任何打开的段——它本身在 flush 取出消息之前,
    当前代码里这段等待前后没有夹着未提交的写,核实现状后确认不需要额外改动,只需在实现时不要把
    段的起点提前到这次等待之前

出处:分段原则见"## Parent"引用的 issue #151 comment;read_delay_window 现状核实自
entry_reactive.py:638-679_debounced_flush 函数体,无写入夹在等待前后)。

三、Drive Tick _evaluate_drive_tick_chatentry_drive_tick.py:332)分段

  • pre-LLM 段:get_relationship_axes(368行)+ compute_unlock_inputs(385行)+
    _evaluate_unified_gate(393行)+(proceed 分支下)get_reconnect_cooldown(415行)+
    gather_profile_facts(426行)+ 视情况的 list_pending_intents/get_pull_consent
    (458/479行,均为纯读)合并进同一段
  • pre-LLM 段在 loop.run_light(...)(522行)调用前提交/关闭
  • post-LLM 段:record_proactive_attempt(539行,proceed 分支)+
    record_unanswered_escalation(552行,条件触发)合并进同一段,loop.run_light() 返回后新开;
    update_reconnect_cooldown(527行)按"七"节改走独立提交,不进这一段
  • 范围边界(写进本票不含糊带过)_evaluate_drive_tick_chat 是被 _drive_tick()
    (APScheduler 作业,entry_drive_tick.py:265,issue #151 原 AC 点名的"5 个作业"之一)逐 chat
    循环调用的(323-329行)——job 级别"如何给每次循环的 _evaluate_drive_tick_chat 调用供应
    session"“循环尾部 clear_recall_timing_override(329行)算哪一段",属于「APScheduler 定时
    作业」姊妹票的职责,本票只负责 _evaluate_drive_tick_chat 函数体内部的分段边界,接收
    姊妹票传入的 session 参数,不负责 job 循环本身怎么开/关 session

出处:分段原则同上;APScheduler 作业清单出处见 #151 body"APScheduler 定时作业...当前 5 个:
entry_callback.py×1、entry_drive_tick.py×1、entry_offline_jobs.py×3"。

四、群聊即刻追问 _run_group_followup_checkentry_reactive.py:813):独立会话 + 分段

  • 函数入口独立开一个新 session,不接住 _debounced_flush(父调用,通过
    _schedule_group_followup_checkasyncio.create_task 派生本函数,797/808行)已经关闭的段
    ——两者不共享事务边界
  • pre-LLM 段:_evaluate_unified_gate(832行)+(proceed 分支)compute_unlock_inputs
    (850行)合并进同一段,在 loop.run_immediate_followup(chat_id)(872行)调用前提交/关闭
  • post-LLM 段:record_proactive_attempt(880行,条件触发)单独成段,loop.run_immediate_followup()
    返回后新开

出处:#151 comments Design-Verify 阶段 [MEDIUM] 发现"遗漏了 2 个结构相同的触发入口"——原文
明确点名"_run_group_followup_check 因为是 asyncio.create_task 发起的脱钩后台任务,还需要
额外考虑'不能参与父调用请求级事务,需独立开自己的 session'"。

五、环境信号 _process_environment_signalenvironment_pipeline.py:56)分段

  • 环境遥测分流分支(93-107行)不属于本票——识别为遥测后转交
    _process_ambient_telemetry_signal,那是不同的入口,见"范围边界"一节
  • pre-LLM 段:apply_tier1_ambient_contagion(139行,内部两读两写,按"一"节接 session)+
    compute_unlock_inputs(150行)+(过关系网解锁档+熔断闸后)get_relationship_axes(171行,
    条件触发)+ _evaluate_unified_gate(179行)合并进同一段
  • is_relationship_network_unlocked 未过(157-162行)与 _pool_is_broken(166-167行)两处
    提前 return 不写决策快照——现状如此(157-162行注释已明确"这里不写决策快照"),核实后确认
    不属于本票要改的行为,不要在分段改造时顺手补上
  • pre-LLM 段在 loop.run_environment_signal(signal)(216行)调用前提交/关闭
  • post-LLM 段:本路径 proceed 分支在 loop.run_environment_signal() 之后只有决策快照一次
    写入(218-226行),按"七"节走独立提交,不需要额外开一个只装决策快照的"段"

出处:分段原则同上;187-199 行现状(verdict≠proceed 早退只写决策快照)核实自
environment_pipeline.py

六、entry_callback.py::_deliver_callback(62行):写后读打断修复

  • delete_callback(100行,写)+ get_fast_affective_state(107行,读)合并进同一段——
    读操作本身不碰外部 I/O,按分段设计应整合进同一段,不各自独立开关
  • 决策快照写入(108-116行)按"七"节走独立提交,不算进上面那一段
  • 范围边界_callback_scan(35行,APScheduler 作业)逐条 callback 循环调用
    _deliver_callback(59行)——job 级别的 session 供应/独立于 nonebot scoped-session 的隔离
    机制,同"三"节一样,属于「APScheduler 定时作业」姊妹票;本票只负责 _deliver_callback
    函数体内部这三步的分段边界

出处:#151 comments Design-Verify 阶段 [HIGH] 发现"即使写方法改对,合并事务窗口内的'读方法'
仍会打断事务"——原文逐字点名这条实例:"entry_callback.py::_deliver_callback
delete_callback(写) → get_fast_affective_state(读,未改造) → _write_decision_snapshot
(写)——中间这次读会导致 delete_callback 被打断",以及分段设计一般原则"这类'写-读-写'若读
操作本身不碰外部 I/O,应该整合进同一段;只有真正外部调用前才切段"("## Parent"引用)。

七、决策记账类写入:独立提交,不进共享分段

  • 沉默预算消费(gating.py::evaluate_gate 内部 storage.update_silence_budget,335/355行)、
    决策快照(gate_pipeline.py::_write_decision_snapshot)、reconnect 冷却记录
    storage.update_reconnect_cooldown本票范围内的调用点仅 _evaluate_drive_tick_chat
    (527行)这一处——核实现状时发现该存储方法在 runtime_loop.py::run() 内部还有另一个独立调用点
    (976行,_full_frozen_snapshot 之后、真正进入工具循环之前),与本票四个入口无关;那处连同它
    所在的整个 RuntimeLoop 内部分段问题不在本票范围,理由见"Not in scope"新增一条)——这三类
    写入改用地基片提供的"独立提交"能力(不接收/不使用调用方传入的共享段 session),不使用"并入
    当前分段"能力;具体独立提交的底层机制(另开 session、还是同一连接上单独提交一个更小的事务
    单元等)由地基片的 session/事务 API 决定,本票只要求这三类写入的调用点选择"独立提交"这一档
  • 因为决策快照现在总是独立提交、不与同一段内其它写入共享未提交状态,_write_decision_snapshot
    原有的 try: ... except Exception: logger.exception(...)(不重新抛出)予以保留、不改成
    重新抛出
    ——它现在只可能真正因为"这次快照写坏了"而失败(不再可能因为"共享段里更早的写入
    已经把事务弄进中止状态"而被误吞),原文档字符串"快照是可观测性设施,不该因为存储抖了一下就
    让一次正常的对话决策失败"这条既有设计意图继续成立,不需要推翻

出处:ADR-0013「更新(2026-08-18,issue #151 Design-Verify)」节,原文:

沉默预算消费(try_consume_silence_budget)、决策快照(decision snapshot)、reconnect 冷却
记录这几类写入……决策:这几类写入在决策产生的那一刻(LLM 调用之前)独立提交,不参与①层
的请求级共享事务。

独立提交这一档的选择同时解决 #151 comments Design-Verify 阶段 [MEDIUM] 发现"_write_decision_ snapshot 吞异常在共享事务下会变成'伪装成功'陷阱"——该发现原文给出的两个选项"改为失败即重新
抛出,或把这次写入排除在共享事务之外",本票选择后者,理由已写在上面 AC 项里。
update_reconnect_cooldownruntime_loop.py 内的第二个调用点,核实现状用
git grep -n "update_reconnect_cooldown(" -- src 确认存在,见下方"Not in scope"新增一条。

八、runtime_loop.py::RuntimeLoop 共用循环体内部的分段(run/run_light/run_callback/run_environment_signal/run_immediate_followup/_run_directive_turn/_run_event_driven_turn 及其共用的 _run_tool_loop

本票四个入口调用的 loop.run*() 不是无副作用的外部黑盒——核实现状发现它自己内部也有需要分段的写入,且这些写入是本票"pre-LLM段/post-LLM段"划分规则覆盖不到的(那条规则只管入口函数自己的写入,管不到被调用方法内部)。四个入口共同依赖 RuntimeLoop,且本票已经在改造它们的调用链路,这部分工作自然并入本票,不再单独开票:

  • run() 方法体前段(record_familiarity_interaction,961行 + update_reconnect_cooldown,976行,均在 _full_frozen_snapshot 之后、真正进入 _run_tool_loop 之前)合并进同一段,在调用 _run_tool_loop 前提交/关闭
  • _run_tool_loop(1331-1458行)内部的 while True: 多轮工具循环:每一轮 self._llm_client.complete(...) 前,若上一轮的工具处理器写入(如 _handle_update_profilewrite_confirmed_fact,1746行;_handle_register_callback/_handle_note_pending_intent/_handle_delegate_task 等)已经产生,必须先提交/关闭该段;工具处理器执行期间产生的写入各自归入"这一轮工具执行"这一段,处理器返回、下一次 complete() 调用前收口——这正是"## Parent"引用的分段原则里"某一轮工具循环内又调用了一次外部 host 工具,这次工具执行前也要先把当前事务收口"这句在 _run_tool_loop 里的具体落地
  • _run_tool_loop 结束后 run()finally 块:_append_delta(2249行起)与条件性的 clear_send_failures(1042行)合并进同一段,作为整个触发处理的最后一段
  • run_light/run_callback/run_environment_signal/run_immediate_followup/_run_directive_turn/_run_event_driven_turn 这几个变体各自复用 _run_tool_loop 的场景,按同样规则分段(大部分写入路径与 run() 共享,逐一核实各自独有的写入点,不要假设跟 run() 完全一致就照搬)
  • 新增测试:模拟 _run_tool_loop 某一轮工具处理器写入已提交后、下一轮 complete() 调用前触发崩溃,断言该轮工具处理器的写入落库、循环未执行到的后续轮次写入不存在——用来钉住"工具循环内部按轮分段"这条规则,不能只在入口函数层面测

出处:#151 comments"拆片前的事务边界设计方向已与用户确认"一节原文(完整版,"## Parent"引用时
省略了关键句):

不是严格"一轮 while True 工具循环一个事务",是严格按"两次外部 I/O 之间"切分——如果某一轮
工具循环内又调用了一次外部 host 工具,这次工具执行前也要先把当前事务收口。

具体行号核实自 runtime_loop.py:961/976/1331-1458/1746/2249/1042。

九、测试:分段崩溃回滚要有"段 N-1 已提交仍在库"的正例

  • 至少针对 _debounced_flush(四个入口里结构最复杂的一个)新增一条端到端测试:让
    loop.run(chat_id, messages) 在 pre-LLM 段已经提交之后抛出异常(例如给 RuntimeLoop.run
    打一个会抛错的桩),断言 pre-LLM 段的写入(如 update_chat_activity/沉默预算消费后的余额)
    确实落库,而 post-LLM 段该有的写入(record_unanswered_escalation/决策快照)不存在
    ——不能只断言"全部回滚"或"全部不回滚"这种更容易但不精确的写法
  • _run_group_followup_check 补一条测试:父调用 _debounced_flush 的 post-LLM 段写入失败/
    抛异常,不影响 _run_group_followup_check 自己独立开的段——用来钉住"四"节的独立 session 要求
    没有沦为纯声明式承诺
  • Drive Tick / 环境信号两个入口至少各补一条测试,验证 pre-LLM 段与 post-LLM 段的写入范围
    按本票"三""五"节划定的边界各自独立可查(不要求重复"八"第一条那么完整的崩溃注入,但要能看出
    两段写入范围确实分开)
  • entry_callback.py::_deliver_callback 补一条测试:get_fast_affective_state(107行)
    期间即使抛出/触发某种失败,delete_callback(100行)已经落库的效果不应被这次读打断——钉住
    "六"节
  • 全量测试通过

出处:主控会话工单原文"测试须防空转……必须构造'段 N 崩溃、段 N-1 已提交的数据仍然在库里'
这个正例,不能只测'全部回滚'或'全部不回滚'这种更容易但不精确的断言"。

Not in scope

  • _handle_group_slow_interruptentry_reactive.py:223,issue #159,ADR-0032"群内插句嘴")
    _process_ambient_telemetry_signalenvironment_pipeline.py:290,issue #153)——核实
    现状发现:gate_pipeline.py 模块文档字符串现状写的是"反应式、Drive Tick、环境信号、即刻追问、
    群内插句嘴(issue #159)条触发路径……按调用点数是处而不是四处",而 environment_ pipeline.py 另有 _process_ambient_telemetry_signal 这第六个调用点(issue #153,与"环境信号"
    同属一条"路径"但是独立函数)。这两处走的是与本票四个入口完全相同的"门控前写→拦截或放行→真实
    LLM/发送→门控后写"结构,也调用本票要改造的 _evaluate_unified_gate/_write_decision_snapshot/
    apply_tier1_ambient_contagion_handle_group_slow_interrupt 直接调用后者)——本票改这几个
    共享函数的签名后,这两处机械上具备了接入分段的前提,但本票不改这两处的调用点本身,不要
    误认为本票完成后这两处已经自动获得事务分段。这是原始 design-verify 阶段(写在"必须覆盖的具体
    发现"里的"遗漏了 2 个入口")尚未预见到的、issue #159/#153 合入之后才出现的范围增长,需要主控
    会话决定是并入本票现在一起做、并入某个姊妹票、还是单开一张后续票
  • _callback_scan/_drive_tick(两个 APScheduler 作业本身)的 job 级 session 隔离机制——姊妹票
    「APScheduler 定时作业」负责,见"三""六"节的范围边界说明
  • storage.py 里 ~42 个写方法的 session 参数改造本身(async with 误用陷阱修复)——地基片负责
  • record_pool_usage 的 SAVEPOINT 需求、delta.py/reflection.py 里另外 3 处"写后读打断"实例——
    分属「Delta 压缩周期」「反思闭环家族」姊妹票
  • Sent Log/Outbox 写入、SendFailureModel 三个方法——ADR-0013 明文豁免/design-verify 阶段裁定排除,
    本票不涉及,不新增任何相关代码

Blocked by

#197(存储层分段事务基建:session 传递契约与 SAVEPOINT 修复)——需要它交付的 session/事务传递
API("join 当前分段"与"独立提交"两种模式都要,见本票"七"节),以及 async with 误用陷阱的
修复方式(本票的分段划分依赖这个底层机制是安全的,不重新设计它)。

## Parent #151(ADR-0013 ①层单一事务边界落地,issue #108 Q1;状态"待拆片")——本片是拆分后 5 片之一, 负责其中「四个门控触发入口」:反应式 `_debounced_flush`、Drive Tick `_evaluate_drive_tick_chat`、 群聊即刻追问 `_run_group_followup_check`、环境信号 `_process_environment_signal`,这四个都经由 共享的 `_evaluate_unified_gate`(`gate_pipeline.py`)/`evaluate_gate`(`gating.py`)机制。 **事务边界方向已改**:#151 comments"拆片前的事务边界设计方向已与用户确认"(2026-08-21)—— 不是原 ADR-0013 字面"整个请求单一长事务",而是**分段**: > 事务只包住连续无外部 I/O 打断的一段纯 DB 读写;一旦下一步要发起任何外部调用(LLM 补全、host > 工具调用),当前事务必须先提交/关闭,下一段写入重新开一个新事务……不是严格"一轮 `while True` > 工具循环一个事务",是严格按"两次外部 I/O 之间"切分……崩溃=自动回滚**当前未提交的那一段**, > 更早已提交的段落不会被追溯撤销。 本票把这个方向落实成四个入口各自的分段边界,**以及它们共同调用的 `RuntimeLoop` 主循环 (`run`/`run_light`/`_run_tool_loop` 等)内部的分段**(四个入口调用的 `loop.run*()` 不是无副作用 黑盒,其内部的多轮工具循环与前后写入同样需要按分段设计处理,见"八"节;四个入口是它唯一的调用方, 不适合另开一片),并接好地基片交付的 session/segment API(不重新设计该 API 本身)。 **核实中发现代码库自 design-verify 阶段以来又新增了两个结构相同的触发入口** (`_handle_group_slow_interrupt`/issue #159/ADR-0032、`_process_ambient_telemetry_signal`/issue #153), 本票会改动它们共用的 `_evaluate_unified_gate`/`apply_tier1_ambient_contagion` 等函数签名,但**不改 这两处调用点本身**——它们的事务分段接线开了独立的后续票(依赖本票),见 Blocked by 同批次的姊妹票。 ## What to build 四个入口目前各自内部按"读→算→写"顺序调用 storage.py 方法,每个方法各自 `get_scoped_session()`+ `async with`+`commit()`,互不共享事务,也没有任何函数把 session 接力传下去。要落实分段设计,需要: 1. `gate_pipeline.py`/`gating.py` 的中转函数接受 session 参数并往下传(这两个模块被四个入口共同 调用,是真正的"贯穿"关卡)。 2. 四个入口各自按"两次外部 I/O 之间"重新划分自己的写入序列为若干段,pre-LLM 段在发起 `loop.run*()` 前提交/关闭,post-LLM 段在 `loop.run*()` 返回后重新开一个。 3. 三类 ADR-0013 明文排除在"共享分段"之外的决策记账写入(沉默预算消费/决策快照/reconnect 冷却 记录)改用地基片提供的"独立提交"能力,不使用"并入当前分段"能力——这个区分同时解决了 design-verify 阶段两条独立发现(entry_callback 写后读打断 + `_write_decision_snapshot` 吞异常)。 4. `_run_group_followup_check` 作为 `asyncio.create_task` 发起的脱钩后台任务,在自己的函数入口 独立开一个新 session,不尝试接住父调用(`_debounced_flush`)已经关闭的段。 **核实说明**:origin/main 现状与 design-verify 阶段发现描述的代码一致——四处 `_evaluate_unified_gate` 调用点(`entry_reactive.py:698`/`:832`、`entry_drive_tick.py:393`、`environment_pipeline.py:179`)、 `entry_callback.py::_deliver_callback` 的 `delete_callback`(100行)→`get_fast_affective_state`(107行)→ `_write_decision_snapshot`(108行) 序列、`_write_decision_snapshot` 的吞异常 `except Exception: logger.exception(...)`(不重新抛出)均逐行核对无误,未发现代码漂移。**但 `gate_pipeline.py` 模块 文档字符串(现状)显示范围已经比 design-verify 阶段发现时更大**——见下方"范围边界"一节。 ## Acceptance criteria **一、共享机制:session 贯穿 `gate_pipeline.py` / `gating.py`** - [ ] `_evaluate_unified_gate`(`gate_pipeline.py:155`)新增可选 session 参数,签名/复用方式与 地基片交付的 session 传递 API 一致(不在本片另造一套调用约定);内部往下传给 `get_effective_fast_state`/`compute_unlock_inputs`/`get_proactivity_offset`/`evaluate_gate` - [ ] `evaluate_gate`(`gating.py:255`)同样新增可选 session 参数,往下传给 `storage.get_silence_budget` - [ ] `apply_tier1_ambient_contagion`(`environment_pipeline.py:233`,`_process_environment_signal` 内部调用)新增可选 session 参数,往下传给它内部的两次读(`get_fast_affective_state`/ `get_slow_affective_state`)与两次写(`update_fast_affective_state`/`update_slow_affective_state`) > 出处:#151 comments Design-Verify 阶段 [HIGH] 发现"session 从触发入口传到 storage.py 的路径 > 缺失"——原文点名 `gate_pipeline.py`(`_evaluate_unified_gate`/`_write_decision_snapshot`)、 > `gating.py` 等中转函数"目前都不接受 session 参数,若不改,storage.py 新增的可选 session 参数 > 永远只会拿到默认 None"。`_write_decision_snapshot` 的处理方式见本票"七"节(独立提交,不接收 > 调用方传入的共享段 session)。 **二、反应式 `_debounced_flush`(`entry_reactive.py:600`)分段** - [ ] pre-LLM 段:`update_chat_activity`(群聊,695行)+ `_evaluate_unified_gate` 内部除沉默预算 写入外的读取,合并进同一段;verdict≠proceed 的早退分支到此为止(决策快照见"七"节,独立提交, 不算这一段) - [ ] pre-LLM 段在 `loop.run(chat_id, messages)`(753行)调用前提交/关闭 - [ ] post-LLM 段:`set_recall_timing_override`/`_schedule_group_followup_check`/ `record_unanswered_escalation`(756-769行)合并进同一段,`loop.run()` 返回后新开 - [ ] `read_delay_window`(675行)等待期间**不持有任何打开的段**——它本身在 flush 取出消息之前, 当前代码里这段等待前后没有夹着未提交的写,核实现状后确认不需要额外改动,只需在实现时不要把 段的起点提前到这次等待之前 > 出处:分段原则见"## Parent"引用的 issue #151 comment;`read_delay_window` 现状核实自 > `entry_reactive.py:638-679`(`_debounced_flush` 函数体,无写入夹在等待前后)。 **三、Drive Tick `_evaluate_drive_tick_chat`(`entry_drive_tick.py:332`)分段** - [ ] pre-LLM 段:`get_relationship_axes`(368行)+ `compute_unlock_inputs`(385行)+ `_evaluate_unified_gate`(393行)+(proceed 分支下)`get_reconnect_cooldown`(415行)+ `gather_profile_facts`(426行)+ 视情况的 `list_pending_intents`/`get_pull_consent` (458/479行,均为纯读)合并进同一段 - [ ] pre-LLM 段在 `loop.run_light(...)`(522行)调用前提交/关闭 - [ ] post-LLM 段:`record_proactive_attempt`(539行,proceed 分支)+ `record_unanswered_escalation`(552行,条件触发)合并进同一段,`loop.run_light()` 返回后新开; `update_reconnect_cooldown`(527行)按"七"节改走独立提交,不进这一段 - [ ] **范围边界(写进本票不含糊带过)**:`_evaluate_drive_tick_chat` 是被 `_drive_tick()` (APScheduler 作业,`entry_drive_tick.py:265`,issue #151 原 AC 点名的"5 个作业"之一)逐 chat 循环调用的(323-329行)——job 级别"如何给每次循环的 `_evaluate_drive_tick_chat` 调用供应 session"“循环尾部 `clear_recall_timing_override`(329行)算哪一段",属于「APScheduler 定时 作业」姊妹票的职责,本票只负责 `_evaluate_drive_tick_chat` 函数体**内部**的分段边界,接收 姊妹票传入的 session 参数,不负责 job 循环本身怎么开/关 session > 出处:分段原则同上;APScheduler 作业清单出处见 #151 body"APScheduler 定时作业...当前 5 个: > `entry_callback.py`×1、`entry_drive_tick.py`×1、`entry_offline_jobs.py`×3"。 **四、群聊即刻追问 `_run_group_followup_check`(`entry_reactive.py:813`):独立会话 + 分段** - [ ] 函数入口**独立开一个新 session**,不接住 `_debounced_flush`(父调用,通过 `_schedule_group_followup_check` 用 `asyncio.create_task` 派生本函数,797/808行)已经关闭的段 ——两者不共享事务边界 - [ ] pre-LLM 段:`_evaluate_unified_gate`(832行)+(proceed 分支)`compute_unlock_inputs` (850行)合并进同一段,在 `loop.run_immediate_followup(chat_id)`(872行)调用前提交/关闭 - [ ] post-LLM 段:`record_proactive_attempt`(880行,条件触发)单独成段,`loop.run_immediate_followup()` 返回后新开 > 出处:#151 comments Design-Verify 阶段 [MEDIUM] 发现"遗漏了 2 个结构相同的触发入口"——原文 > 明确点名"`_run_group_followup_check` 因为是 `asyncio.create_task` 发起的脱钩后台任务,还需要 > 额外考虑'不能参与父调用请求级事务,需独立开自己的 session'"。 **五、环境信号 `_process_environment_signal`(`environment_pipeline.py:56`)分段** - [ ] **环境遥测分流分支(93-107行)不属于本票**——识别为遥测后转交 `_process_ambient_telemetry_signal`,那是不同的入口,见"范围边界"一节 - [ ] pre-LLM 段:`apply_tier1_ambient_contagion`(139行,内部两读两写,按"一"节接 session)+ `compute_unlock_inputs`(150行)+(过关系网解锁档+熔断闸后)`get_relationship_axes`(171行, 条件触发)+ `_evaluate_unified_gate`(179行)合并进同一段 - [ ] `is_relationship_network_unlocked` 未过(157-162行)与 `_pool_is_broken`(166-167行)两处 提前 return **不写决策快照**——现状如此(157-162行注释已明确"这里不写决策快照"),核实后确认 不属于本票要改的行为,不要在分段改造时顺手补上 - [ ] pre-LLM 段在 `loop.run_environment_signal(signal)`(216行)调用前提交/关闭 - [ ] post-LLM 段:本路径 proceed 分支在 `loop.run_environment_signal()` 之后只有决策快照一次 写入(218-226行),按"七"节走独立提交,不需要额外开一个只装决策快照的"段" > 出处:分段原则同上;187-199 行现状(verdict≠proceed 早退只写决策快照)核实自 > `environment_pipeline.py`。 **六、`entry_callback.py::_deliver_callback`(62行):写后读打断修复** - [ ] `delete_callback`(100行,写)+ `get_fast_affective_state`(107行,读)合并进同一段—— 读操作本身不碰外部 I/O,按分段设计应整合进同一段,不各自独立开关 - [ ] 决策快照写入(108-116行)按"七"节走独立提交,不算进上面那一段 - [ ] **范围边界**:`_callback_scan`(35行,APScheduler 作业)逐条 callback 循环调用 `_deliver_callback`(59行)——job 级别的 session 供应/独立于 nonebot scoped-session 的隔离 机制,同"三"节一样,属于「APScheduler 定时作业」姊妹票;本票只负责 `_deliver_callback` 函数体内部这三步的分段边界 > 出处:#151 comments Design-Verify 阶段 [HIGH] 发现"即使写方法改对,合并事务窗口内的'读方法' > 仍会打断事务"——原文逐字点名这条实例:"`entry_callback.py::_deliver_callback`: > `delete_callback`(写) → `get_fast_affective_state`(读,未改造) → `_write_decision_snapshot` > (写)——中间这次读会导致 `delete_callback` 被打断",以及分段设计一般原则"这类'写-读-写'若读 > 操作本身不碰外部 I/O,应该整合进同一段;只有真正外部调用前才切段"("## Parent"引用)。 **七、决策记账类写入:独立提交,不进共享分段** - [ ] 沉默预算消费(`gating.py::evaluate_gate` 内部 `storage.update_silence_budget`,335/355行)、 决策快照(`gate_pipeline.py::_write_decision_snapshot`)、reconnect 冷却记录 (`storage.update_reconnect_cooldown`,**本票范围内**的调用点仅 `_evaluate_drive_tick_chat` (527行)这一处——核实现状时发现该存储方法在 `runtime_loop.py::run()` 内部还有另一个独立调用点 (976行,`_full_frozen_snapshot` 之后、真正进入工具循环之前),与本票四个入口无关;那处连同它 所在的整个 `RuntimeLoop` 内部分段问题不在本票范围,理由见"Not in scope"新增一条)——这三类 写入改用地基片提供的"独立提交"能力(不接收/不使用调用方传入的共享段 session),不使用"并入 当前分段"能力;具体独立提交的底层机制(另开 session、还是同一连接上单独提交一个更小的事务 单元等)由地基片的 session/事务 API 决定,本票只要求这三类写入的调用点选择"独立提交"这一档 - [ ] 因为决策快照现在总是独立提交、不与同一段内其它写入共享未提交状态,`_write_decision_snapshot` 原有的 `try: ... except Exception: logger.exception(...)`(不重新抛出)**予以保留、不改成 重新抛出**——它现在只可能真正因为"这次快照写坏了"而失败(不再可能因为"共享段里更早的写入 已经把事务弄进中止状态"而被误吞),原文档字符串"快照是可观测性设施,不该因为存储抖了一下就 让一次正常的对话决策失败"这条既有设计意图继续成立,不需要推翻 > 出处:ADR-0013「更新(2026-08-18,issue #151 Design-Verify)」节,原文: > > 沉默预算消费(`try_consume_silence_budget`)、决策快照(decision snapshot)、reconnect 冷却 > > 记录这几类写入……**决策:这几类写入在决策产生的那一刻(LLM 调用之前)独立提交,不参与①层 > > 的请求级共享事务。** > > 独立提交这一档的选择同时解决 #151 comments Design-Verify 阶段 [MEDIUM] 发现"`_write_decision_ > snapshot` 吞异常在共享事务下会变成'伪装成功'陷阱"——该发现原文给出的两个选项"改为失败即重新 > 抛出,或把这次写入排除在共享事务之外",本票选择后者,理由已写在上面 AC 项里。 > `update_reconnect_cooldown` 在 `runtime_loop.py` 内的第二个调用点,核实现状用 > `git grep -n "update_reconnect_cooldown(" -- src` 确认存在,见下方"Not in scope"新增一条。 **八、`runtime_loop.py::RuntimeLoop` 共用循环体内部的分段(`run`/`run_light`/`run_callback`/`run_environment_signal`/`run_immediate_followup`/`_run_directive_turn`/`_run_event_driven_turn` 及其共用的 `_run_tool_loop`)** 本票四个入口调用的 `loop.run*()` 不是无副作用的外部黑盒——核实现状发现它自己内部也有需要分段的写入,且这些写入是本票"pre-LLM段/post-LLM段"划分规则覆盖不到的(那条规则只管入口函数自己的写入,管不到被调用方法内部)。四个入口共同依赖 `RuntimeLoop`,且本票已经在改造它们的调用链路,这部分工作自然并入本票,不再单独开票: - [ ] `run()` 方法体前段(`record_familiarity_interaction`,961行 + `update_reconnect_cooldown`,976行,均在 `_full_frozen_snapshot` 之后、真正进入 `_run_tool_loop` 之前)合并进同一段,在调用 `_run_tool_loop` 前提交/关闭 - [ ] `_run_tool_loop`(1331-1458行)内部的 `while True:` 多轮工具循环:每一轮 `self._llm_client.complete(...)` 前,若上一轮的工具处理器写入(如 `_handle_update_profile` 的 `write_confirmed_fact`,1746行;`_handle_register_callback`/`_handle_note_pending_intent`/`_handle_delegate_task` 等)已经产生,必须先提交/关闭该段;工具处理器执行期间产生的写入各自归入"这一轮工具执行"这一段,处理器返回、下一次 `complete()` 调用前收口——这正是"## Parent"引用的分段原则里"某一轮工具循环内又调用了一次外部 host 工具,这次工具执行前也要先把当前事务收口"这句在 `_run_tool_loop` 里的具体落地 - [ ] `_run_tool_loop` 结束后 `run()` 的 `finally` 块:`_append_delta`(2249行起)与条件性的 `clear_send_failures`(1042行)合并进同一段,作为整个触发处理的最后一段 - [ ] `run_light`/`run_callback`/`run_environment_signal`/`run_immediate_followup`/`_run_directive_turn`/`_run_event_driven_turn` 这几个变体各自复用 `_run_tool_loop` 的场景,按同样规则分段(大部分写入路径与 `run()` 共享,逐一核实各自独有的写入点,不要假设跟 `run()` 完全一致就照搬) - [ ] 新增测试:模拟 `_run_tool_loop` 某一轮工具处理器写入已提交后、下一轮 `complete()` 调用前触发崩溃,断言该轮工具处理器的写入落库、循环未执行到的后续轮次写入不存在——用来钉住"工具循环内部按轮分段"这条规则,不能只在入口函数层面测 > 出处:#151 comments"拆片前的事务边界设计方向已与用户确认"一节原文(完整版,"## Parent"引用时 > 省略了关键句): > > 不是严格"一轮 `while True` 工具循环一个事务",是严格按"两次外部 I/O 之间"切分——如果某一轮 > > 工具循环内又调用了一次外部 host 工具,这次工具执行前也要先把当前事务收口。 > > 具体行号核实自 `runtime_loop.py`:961/976/1331-1458/1746/2249/1042。 **九、测试:分段崩溃回滚要有"段 N-1 已提交仍在库"的正例** - [ ] 至少针对 `_debounced_flush`(四个入口里结构最复杂的一个)新增一条端到端测试:让 `loop.run(chat_id, messages)` 在 pre-LLM 段已经提交之后抛出异常(例如给 `RuntimeLoop.run` 打一个会抛错的桩),断言 pre-LLM 段的写入(如 `update_chat_activity`/沉默预算消费后的余额) **确实落库**,而 post-LLM 段该有的写入(`record_unanswered_escalation`/决策快照)**不存在** ——不能只断言"全部回滚"或"全部不回滚"这种更容易但不精确的写法 - [ ] `_run_group_followup_check` 补一条测试:父调用 `_debounced_flush` 的 post-LLM 段写入失败/ 抛异常,不影响 `_run_group_followup_check` 自己独立开的段——用来钉住"四"节的独立 session 要求 没有沦为纯声明式承诺 - [ ] Drive Tick / 环境信号两个入口至少各补一条测试,验证 pre-LLM 段与 post-LLM 段的写入范围 按本票"三""五"节划定的边界各自独立可查(不要求重复"八"第一条那么完整的崩溃注入,但要能看出 两段写入范围确实分开) - [ ] `entry_callback.py::_deliver_callback` 补一条测试:`get_fast_affective_state`(107行) 期间即使抛出/触发某种失败,`delete_callback`(100行)已经落库的效果不应被这次读打断——钉住 "六"节 - [ ] 全量测试通过 > 出处:主控会话工单原文"测试须防空转……必须构造'段 N 崩溃、段 N-1 已提交的数据仍然在库里' > 这个正例,不能只测'全部回滚'或'全部不回滚'这种更容易但不精确的断言"。 ## Not in scope - **`_handle_group_slow_interrupt`(`entry_reactive.py:223`,issue #159,ADR-0032"群内插句嘴")** 与 **`_process_ambient_telemetry_signal`(`environment_pipeline.py:290`,issue #153)**——核实 现状发现:`gate_pipeline.py` 模块文档字符串现状写的是"反应式、Drive Tick、环境信号、即刻追问、 群内插句嘴(issue #159)**五**条触发路径……按调用点数是**五**处而不是四处",而 `environment_ pipeline.py` 另有 `_process_ambient_telemetry_signal` 这第六个调用点(issue #153,与"环境信号" 同属一条"路径"但是独立函数)。这两处走的是与本票四个入口完全相同的"门控前写→拦截或放行→真实 LLM/发送→门控后写"结构,也调用本票要改造的 `_evaluate_unified_gate`/`_write_decision_snapshot`/ `apply_tier1_ambient_contagion`(`_handle_group_slow_interrupt` 直接调用后者)——本票改这几个 共享函数的签名后,这两处**机械上具备了接入分段的前提**,但本票不改这两处的调用点本身,不要 误认为本票完成后这两处已经自动获得事务分段。这是原始 design-verify 阶段(写在"必须覆盖的具体 发现"里的"遗漏了 2 个入口")尚未预见到的、issue #159/#153 合入之后才出现的范围增长,需要主控 会话决定是并入本票现在一起做、并入某个姊妹票、还是单开一张后续票 - `_callback_scan`/`_drive_tick`(两个 APScheduler 作业本身)的 job 级 session 隔离机制——姊妹票 「APScheduler 定时作业」负责,见"三""六"节的范围边界说明 - storage.py 里 ~42 个写方法的 session 参数改造本身(`async with` 误用陷阱修复)——地基片负责 - `record_pool_usage` 的 SAVEPOINT 需求、delta.py/reflection.py 里另外 3 处"写后读打断"实例—— 分属「Delta 压缩周期」「反思闭环家族」姊妹票 - Sent Log/Outbox 写入、SendFailureModel 三个方法——ADR-0013 明文豁免/design-verify 阶段裁定排除, 本票不涉及,不新增任何相关代码 ## Blocked by #197(存储层分段事务基建:session 传递契约与 SAVEPOINT 修复)——需要它交付的 session/事务传递 API("join 当前分段"与"独立提交"两种模式都要,见本票"七"节),以及 `async with` 误用陷阱的 修复方式(本票的分段划分依赖这个底层机制是安全的,不重新设计它)。
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#198
No description provided.