self_recall/edit平台撤回窗口+per-chat速率预算 #115

Closed
opened 2026-08-11 03:36:41 +00:00 by KumaAgent · 0 comments
Member

Parent

#103 第四批清单:31 条实现类落差 + 9 条待拍板(条目 17、18;顺带吸收一处同型未归宿的缺口)

What to build

ADR-0012 的"自撤/改口"机制默认 ON,其安全性建立在两根支柱上:硬约束(chat_id 归属校验,已由 issue #102 补上)与速率预算(per-chat 撤回/编辑动作速率上限)。速率预算这一根从未落地:模型在同一次 assistant 回合里可以批量发上百个 self_recall 调用,三道既有上限(self_recall≤1/edit≤N/MAX_SENDS_PER_TURN)一道不触发,run_limit 也不扣(SELF_RECALL/EDIT 都在 RUN_LIMIT_EXEMPT_TOOL_NAMES 里,且计数按轮不按调用),core 会老实执行上百次 egress.recall,撞上 QQ 撤回频控。

同时,"平台撤回窗口"这半也从未落地——超过平台撤回窗口(QQ/OneBot v11 是 2 分钟)的句柄仍会被尝试 delete_msg。这依赖 Sent Log 补上时间字段,而 ADR-0012 §7 早已把这个字段定义为"既定后果",不是一个外部前置。

两者落在同样两个函数(_handle_self_recall/_handle_edit)、同样两道闸位(都必须在能力位分叉之前),同片做。

Acceptance criteria

一、Sent Log 加时间字段(两者共同的基建前提,本片自带,不等外部票)

  • sent_log.SentLogEntry 新增时间字段(建议 sent_at: datetime,避免与 DeltaEntry.time/EventRecord.time 混淆)
  • storage.SentLogModel 加对应列;sent_log.StoragePort.record() 协议签名、内存实现 SentLog.record()、持久化实现 PersistentStorage.record()_to_entry() 均适配;唯一写入点 RuntimeLoop._flush() 补传时间
  • 本仓不随包分发 alembic 迁移(migrations/versions/ 只有 __init__.py,真实 schema 由 host 迁移链管理),加可空列不需要在本仓补迁移文件
  • 存量 Sent Log 行没有时间值,缺省行为要显式定义(别让旧句柄因 sent_at=None 被静默判成超窗口或静默放行)

原文(docs/adr/0012-self-message-control-and-tool-driven-output.md「决策 → 7. 基础设施(既定后果)」):

已发日志(Sent Log)句柄 ↔ 平台 message_id / content / chat / time / send-state,storage port。同源治旧 send_record 时序竞态(旁路动作统一走此账本)。

二、平台撤回窗口硬约束

  • _handle_self_recall/_handle_edit 拒绝已超出平台撤回窗口的句柄,窗口值进静态 config
  • pre-send 分支不受窗口约束——两个 handler 开头 if handle in state.pending 那条分支是 §1「纯 core、无平台调用、总是可用、零风控」,窗口闸只管已发路径
  • 超窗口 fail-closed、零平台调用,同既有 "handle not found or already recalled" 风格返回错误串;模型的退路是改用普通 send_message 发纠正文本(ADR §5 已指定)

原文(docs/adr/0012「决策 → 2. 触发」节):

控制意图只在生成输出的前台轮次产生……改当轮 outbox 段、或撤/改近期已发消息(限当前会话 + 平台撤回窗口,允许"下一轮才反应过来撤上一轮")。

原文(同 ADR「决策 → 5. 编辑降级」节):

"更正消息"不是特殊机制 = 模型改用普通 send_message 发纠正文本(更想那样、或 recall 已过窗口/超预算时)。

三、per-chat 动作速率预算

  • 新增 per-chat 动作速率预算(撤回/编辑合计),形状仿既有 SilenceBudgetState/refill_silence_budget 令牌桶先例:新 dataclass + 纯函数 + 存储 port 切片(内存 + 持久化两个实现 + 一张新表)
  • 超预算是能力降级不是异常:工具返回 rate-limited(同既有 render_tool_result(call.id, error=...) 风格给稳定错误串),fail-closed 零平台调用,模型"不带 recall 继续"
  • 预算只管 post-send——pre-send 分支绝不能扣预算
  • _handle_edit 的降级路径(无原生 edit → recall+resend)第一步就是 egress.recall它也要过这道闸,且闸位与既有归属闸同侧(能力位分叉之前)
  • 具体数字按 ADR-0011"本轮不拍任何具体数字"保守默认起步,不在本片标定
  • 测试须把 per-chat 计数真正推穿并断言 egress 调用次数为零(不是只断言"没有输出结果"——那在多种原因下都成立)

原文(docs/adr/0012「决策 → 6. 治理:默认 ON + 速率预算兜底」节):

速率预算(主兜底):per-chat 撤回/编辑动作速率上限,尊重 QQ 撤回频控;超预算该能力降级(工具返回 rate-limited,模型不带 recall 继续)。此为动作速率预算,区别于 ADR-0010 的 LLM token 预算。

  • 不要顺带复核"默认 ON 是否维持"——那是 ADR-0012「拒绝了」节点名否掉的备选(「默认 OFF + 晚解锁 / 需 admin 显开」),速率预算落地是把"默认 ON"的前提补齐,不是给它添疑点

四、顺带吸收:send_messagequote_message_id 归属校验缺口

issue #102 只补了 _handle_self_recall/_handle_edit 两处的 chat_id 归属校验,send_messagequote_message_id 参数(模型直给的平台 id、不经 Sent Log)没有同类校验,且该缺口目前在 #103/#101/#108 全文零命中、没有任何票认领。与本片同属"句柄/引用跨 chat 误用"这一类问题,顺带一起补:

  • quote_message_id 校验其所引用消息确属当前 chat,跨 chat 引用时按既有能力门控"无该能力则退化为不带引用的普通发送"的降级模式处理,不报错阻断对话

Not in scope

  • §6 最后一条"每次 self_recall/edit 记决策快照"——已由 #101 留痕第 8 条认领并缓做(DecisionSnapshot 按"一轮触发"建模,塞进去要动数据形状),不在本片顺带实现

Blocked by

None — can start immediately

## Parent #103 第四批清单:31 条实现类落差 + 9 条待拍板(条目 17、18;顺带吸收一处同型未归宿的缺口) ## What to build ADR-0012 的"自撤/改口"机制默认 ON,其安全性建立在两根支柱上:**硬约束**(chat_id 归属校验,已由 issue #102 补上)与**速率预算**(per-chat 撤回/编辑动作速率上限)。速率预算这一根从未落地:模型在同一次 assistant 回合里可以批量发上百个 `self_recall` 调用,三道既有上限(`self_recall≤1`/`edit≤N`/`MAX_SENDS_PER_TURN`)一道不触发,`run_limit` 也不扣(`SELF_RECALL`/`EDIT` 都在 `RUN_LIMIT_EXEMPT_TOOL_NAMES` 里,且计数按轮不按调用),core 会老实执行上百次 `egress.recall`,撞上 QQ 撤回频控。 同时,"平台撤回窗口"这半也从未落地——超过平台撤回窗口(QQ/OneBot v11 是 2 分钟)的句柄仍会被尝试 `delete_msg`。这依赖 Sent Log 补上时间字段,而 ADR-0012 §7 早已把这个字段定义为"既定后果",不是一个外部前置。 两者落在同样两个函数(`_handle_self_recall`/`_handle_edit`)、同样两道闸位(都必须在能力位分叉之前),同片做。 ## Acceptance criteria **一、Sent Log 加时间字段(两者共同的基建前提,本片自带,不等外部票)** - [ ] `sent_log.SentLogEntry` 新增时间字段(建议 `sent_at: datetime`,避免与 `DeltaEntry.time`/`EventRecord.time` 混淆) - [ ] `storage.SentLogModel` 加对应列;`sent_log.StoragePort.record()` 协议签名、内存实现 `SentLog.record()`、持久化实现 `PersistentStorage.record()` 与 `_to_entry()` 均适配;唯一写入点 `RuntimeLoop._flush()` 补传时间 - [ ] 本仓不随包分发 alembic 迁移(`migrations/versions/` 只有 `__init__.py`,真实 schema 由 host 迁移链管理),加可空列不需要在本仓补迁移文件 - [ ] 存量 Sent Log 行没有时间值,缺省行为要显式定义(别让旧句柄因 `sent_at=None` 被静默判成超窗口或静默放行) 原文(docs/adr/0012-self-message-control-and-tool-driven-output.md「决策 → 7. 基础设施(既定后果)」): > **已发日志(Sent Log)**:`句柄 ↔ 平台 message_id / content / chat / time / send-state`,storage port。同源治旧 `send_record 时序竞态`(旁路动作统一走此账本)。 **二、平台撤回窗口硬约束** - [ ] `_handle_self_recall`/`_handle_edit` 拒绝已超出平台撤回窗口的句柄,窗口值进静态 config - [ ] **pre-send 分支不受窗口约束**——两个 handler 开头 `if handle in state.pending` 那条分支是 §1「纯 core、无平台调用、总是可用、零风控」,窗口闸只管已发路径 - [ ] 超窗口 fail-closed、零平台调用,同既有 `"handle not found or already recalled"` 风格返回错误串;模型的退路是改用普通 `send_message` 发纠正文本(ADR §5 已指定) 原文(docs/adr/0012「决策 → 2. 触发」节): > 控制意图只在**生成输出的前台轮次**产生……改当轮 outbox 段、或撤/改**近期已发**消息(限当前会话 + 平台撤回窗口,允许"下一轮才反应过来撤上一轮")。 原文(同 ADR「决策 → 5. 编辑降级」节): > "更正消息"不是特殊机制 = 模型改用普通 `send_message` 发纠正文本(更想那样、或 recall 已过窗口/超预算时)。 **三、per-chat 动作速率预算** - [ ] 新增 per-chat 动作速率预算(撤回/编辑合计),形状仿既有 `SilenceBudgetState`/`refill_silence_budget` 令牌桶先例:新 dataclass + 纯函数 + 存储 port 切片(内存 + 持久化两个实现 + 一张新表) - [ ] 超预算是**能力降级**不是异常:工具返回 rate-limited(同既有 `render_tool_result(call.id, error=...)` 风格给稳定错误串),fail-closed 零平台调用,模型"不带 recall 继续" - [ ] 预算只管 post-send——pre-send 分支绝不能扣预算 - [ ] `_handle_edit` 的降级路径(无原生 edit → recall+resend)第一步就是 `egress.recall`,**它也要过这道闸**,且闸位与既有归属闸同侧(能力位分叉之前) - [ ] 具体数字按 ADR-0011"本轮不拍任何具体数字"保守默认起步,不在本片标定 - [ ] 测试须把 per-chat 计数真正推穿并断言 `egress` 调用次数为零(不是只断言"没有输出结果"——那在多种原因下都成立) 原文(docs/adr/0012「决策 → 6. 治理:默认 ON + 速率预算兜底」节): > **速率预算**(主兜底):per-chat 撤回/编辑动作速率上限,尊重 QQ 撤回频控;超预算该能力降级(工具返回 rate-limited,模型不带 recall 继续)。此为**动作速率**预算,区别于 ADR-0010 的 LLM token 预算。 - [ ] **不要**顺带复核"默认 ON 是否维持"——那是 ADR-0012「拒绝了」节点名否掉的备选(「默认 OFF + 晚解锁 / 需 admin 显开」),速率预算落地是把"默认 ON"的前提**补齐**,不是给它添疑点 **四、顺带吸收:`send_message` 的 `quote_message_id` 归属校验缺口** issue #102 只补了 `_handle_self_recall`/`_handle_edit` 两处的 chat_id 归属校验,`send_message` 的 `quote_message_id` 参数(模型直给的平台 id、不经 Sent Log)没有同类校验,且该缺口目前在 #103/#101/#108 全文零命中、没有任何票认领。与本片同属"句柄/引用跨 chat 误用"这一类问题,顺带一起补: - [ ] `quote_message_id` 校验其所引用消息确属当前 chat,跨 chat 引用时按既有能力门控"无该能力则退化为不带引用的普通发送"的降级模式处理,不报错阻断对话 ## Not in scope - §6 最后一条"每次 `self_recall`/`edit` 记决策快照"——已由 #101 留痕第 8 条认领并缓做(DecisionSnapshot 按"一轮触发"建模,塞进去要动数据形状),不在本片顺带实现 ## Blocked by None — can start immediately
Yushu closed this issue 2026-08-12 05:13:01 +00:00
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#115
No description provided.