四个门控触发入口 + RuntimeLoop主循环的事务分段接线 #198
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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 字面"整个请求单一长事务",而是分段:
本票把这个方向落实成四个入口各自的分段边界,以及它们共同调用的
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 接力传下去。要落实分段设计,需要:gate_pipeline.py/gating.py的中转函数接受 session 参数并往下传(这两个模块被四个入口共同调用,是真正的"贯穿"关卡)。
loop.run*()前提交/关闭,post-LLM 段在loop.run*()返回后重新开一个。记录)改用地基片提供的"独立提交"能力,不使用"并入当前分段"能力——这个区分同时解决了
design-verify 阶段两条独立发现(entry_callback 写后读打断 +
_write_decision_snapshot吞异常)。_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_gateevaluate_gate(gating.py:255)同样新增可选 session 参数,往下传给storage.get_silence_budgetapply_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)二、反应式
_debounced_flush(entry_reactive.py:600)分段update_chat_activity(群聊,695行)+_evaluate_unified_gate内部除沉默预算写入外的读取,合并进同一段;verdict≠proceed 的早退分支到此为止(决策快照见"七"节,独立提交,
不算这一段)
loop.run(chat_id, messages)(753行)调用前提交/关闭set_recall_timing_override/_schedule_group_followup_check/record_unanswered_escalation(756-769行)合并进同一段,loop.run()返回后新开read_delay_window(675行)等待期间不持有任何打开的段——它本身在 flush 取出消息之前,当前代码里这段等待前后没有夹着未提交的写,核实现状后确认不需要额外改动,只需在实现时不要把
段的起点提前到这次等待之前
三、Drive Tick
_evaluate_drive_tick_chat(entry_drive_tick.py:332)分段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行,均为纯读)合并进同一段
loop.run_light(...)(522行)调用前提交/关闭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
四、群聊即刻追问
_run_group_followup_check(entry_reactive.py:813):独立会话 + 分段_debounced_flush(父调用,通过_schedule_group_followup_check用asyncio.create_task派生本函数,797/808行)已经关闭的段——两者不共享事务边界
_evaluate_unified_gate(832行)+(proceed 分支)compute_unlock_inputs(850行)合并进同一段,在
loop.run_immediate_followup(chat_id)(872行)调用前提交/关闭record_proactive_attempt(880行,条件触发)单独成段,loop.run_immediate_followup()返回后新开
五、环境信号
_process_environment_signal(environment_pipeline.py:56)分段_process_ambient_telemetry_signal,那是不同的入口,见"范围边界"一节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行注释已明确"这里不写决策快照"),核实后确认
不属于本票要改的行为,不要在分段改造时顺手补上
loop.run_environment_signal(signal)(216行)调用前提交/关闭loop.run_environment_signal()之后只有决策快照一次写入(218-226行),按"七"节走独立提交,不需要额外开一个只装决策快照的"段"
六、
entry_callback.py::_deliver_callback(62行):写后读打断修复delete_callback(100行,写)+get_fast_affective_state(107行,读)合并进同一段——读操作本身不碰外部 I/O,按分段设计应整合进同一段,不各自独立开关
_callback_scan(35行,APScheduler 作业)逐条 callback 循环调用_deliver_callback(59行)——job 级别的 session 供应/独立于 nonebot scoped-session 的隔离机制,同"三"节一样,属于「APScheduler 定时作业」姊妹票;本票只负责
_deliver_callback函数体内部这三步的分段边界
七、决策记账类写入:独立提交,不进共享分段
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(...)(不重新抛出)予以保留、不改成重新抛出——它现在只可能真正因为"这次快照写坏了"而失败(不再可能因为"共享段里更早的写入
已经把事务弄进中止状态"而被误吞),原文档字符串"快照是可观测性设施,不该因为存储抖了一下就
让一次正常的对话决策失败"这条既有设计意图继续成立,不需要推翻
八、
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()调用前触发崩溃,断言该轮工具处理器的写入落库、循环未执行到的后续轮次写入不存在——用来钉住"工具循环内部按轮分段"这条规则,不能只在入口函数层面测九、测试:分段崩溃回滚要有"段 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 要求没有沦为纯声明式承诺
按本票"三""五"节划定的边界各自独立可查(不要求重复"八"第一条那么完整的崩溃注入,但要能看出
两段写入范围确实分开)
entry_callback.py::_deliver_callback补一条测试:get_fast_affective_state(107行)期间即使抛出/触发某种失败,
delete_callback(100行)已经落库的效果不应被这次读打断——钉住"六"节
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 定时作业」负责,见"三""六"节的范围边界说明
async with误用陷阱修复)——地基片负责record_pool_usage的 SAVEPOINT 需求、delta.py/reflection.py 里另外 3 处"写后读打断"实例——分属「Delta 压缩周期」「反思闭环家族」姊妹票
本票不涉及,不新增任何相关代码
Blocked by
#197(存储层分段事务基建:session 传递契约与 SAVEPOINT 修复)——需要它交付的 session/事务传递
API("join 当前分段"与"独立提交"两种模式都要,见本票"七"节),以及
async with误用陷阱的修复方式(本票的分段划分依赖这个底层机制是安全的,不重新设计它)。