交互原语改 host 注册开放 registry + 移除四个能力位(ADR-0030 整份落地) #109

Merged
Yushu merged 2 commits from feat/104-open-interaction-registry into main 2026-08-03 05:10:50 +00:00
Member

Closes #104

ADR-0030 整份落地。core 不再理解 poke/reaction 这两个 QQ 概念:交互原语的
渲染改成 host 按 kind 注册的开放 registry,四个具名能力位、两个 core 工具、
两个 EgressPort 方法一并删除。趁两个 host 仓对 arise 零命中做这批破坏性变更。

交付

  • ConversationInput.interaction 改开放形状 {kind, target_is_self, actor_id, target_id, payload},封闭的 InteractionKind Literal 删除;旧的
    target_message_id/emoji 收进 payload
  • 新增 ArisePorts.interaction_renderers;core 只拥固定信封
    <interaction kind=… sender_id=… sender_name=… time=…>{host 渲染内容}</interaction>
  • 新增 IngressPort.to_interaction + 一个 @event_postprocessor 入口,
    让这条链在生产里真的可达。
  • target_is_self=True → 统一评估管线;False → 转 EnvironmentSignal 走旁观。
  • 删除 poke/poke_send/reaction_receive/reaction_send 四个能力位、
    POKE_SEND/REACTION_SEND 两个 core 工具、EgressPort.poke_send/reaction_send

三处文档没说、按 grill 结论做的

  1. actor_id/target_id 照 ADR 建全五个字段。 它们一度看着像"零读者字段"
    (本仓点名过的反模式),review 之后不再是:第三方那条路把它们喂给
    EnvironmentSignal.actor_id/subject_id,正是 ADR-0030 决策节点名的那层复用。
  2. 入口建了,不只交契约。 新增一个 ingress port 方法同样是破坏性变更,
    零 host 的今天免费,ob11 接入后就是两边协调迁移——与本票自己的立论一致。
    测试适配器给了真实的 OB11 PokeNotifyEvent 实现(不是像其它几个 stub 那样
    恒返回 None),所以整条链在测试里是真的走通的。
  3. 未注册 kind 的行为沿用 tags.py 先例:整条剔除、不留占位。

两轴 review:13 条 finding 全部存活,0 条被驳回

blocker(两轴独立撞上同一处):「未注册 kind 整条剔除」原先只做在渲染层,
入口不查 registry——事件照样入队、跑完两级门控、发起一次 LLM 调用,而那一轮的
context 里一条 user 消息都没有,模型凭空编一句发出去。空 registry 是
ports.py 明说的合法配置("不是错误配置"),所以这是任何没注册交互原语的部署
都会撞上的路径。与 tags.py 那条先例的差别在于:未注册的 inline 标签只是从
已经决定要发的一段文本里被抠掉,而这里被抠掉的是触发本身的全部内容
闸已提到入口。

Spec 轴抓到我写了一个可证伪的理由target_is_self=False 原先只做了"不进
反应式批次",我给的不做理由是"core 造不出 EnvironmentSignal,那个类型要
plugin/action"。复核实跑推翻了它——action 就是 kindchat_id/time
都在手上,只差 plugin 一个 core 自选常量。这属于本仓记过七次的「给不做的分支
配一个听起来合理但错误的理由」,第八次。已按 ADR 实现完整路由。

其余:5 处把已删符号当现存事物引用的散文(含两处过期计数「六个裸的平台调用」,
按 issue #88/#102 确立的处置删数字而不是改数字)、一条承诺大于覆盖的守卫
docstring、一段被抽走后悬空的注释。

有意不改,交评估侧

戳一戳不撤销沉默窗口。chat_id 上有一条不经 _pending_flushes
待发消息正在沉默窗口里等(环境信号 tier-2 / Callback 送回 / 即刻追问三条路都是
这样),一次戳一戳不会撤掉它,于是那条和本轮回复会一前一后都发出去;换成一条
普通消息则会被 _interrupt_silence_window_on_any_message 撤掉。

不改的理由:CONTEXT.md 给沉默窗口写的触发条件是"窗口内这个 chat 里任何人发言
即撤销,补位而非抢话",戳一戳不是发言——把它算进去是扩展一条已拍板机制的触发面,
不该由实现方在本票里顺手定。理由已记在 _enqueue_for_debounced_turn 的 docstring
里。这条需要评估侧决定要不要开票。

同一形状的相邻缺口_handle_environment_signal_handle_presence_events
同样直接 await host 回调、同样没有 try/except,而 _apply_event_postprocessors
全部 postprocessor 交给同一个 anyio.create_task_group()——任一子任务抛出会取消
同组其余任务(外层那个 catch 只记日志)。本票给自己新加的入口兜住了,那两条
既有的没动,同样交评估侧

验证

  • 全量 1541 passed(新增 13),ruff check 干净。
  • 10 项变异全部按预期变红:去掉 target_is_self 半闸 / 去掉 interaction is None
    半闸 / 未注册 kind 返回空信封 / 去掉上下文组装的 None 过滤 / 信封丢掉 kind 属性 /
    去掉入口 try/except / 把 poke_send 无条件加回工具表 / 把 poke 能力位加回来 /
    去掉入口的 registry 闸 / 第三方分支改回直接丢弃。
    每次变异都先断言"真写进去了",还原后 md5 全部一致、哨兵残留 0。

尚未做

文档同步(docs 分支,按规则永不推送):ADR-0030 追加"已实现"更新节、
ADR-0004 与它的矛盾、CONTEXT.md 两条术语,以及 review 补出来的
ADR-0012 两处过期计数与 ADR-0020:125 的过期先例引用。落在后续提交。

🤖 Generated with Claude Code

Closes #104 ADR-0030 整份落地。core 不再理解 `poke`/`reaction` 这两个 QQ 概念:交互原语的 渲染改成 host 按 `kind` 注册的开放 registry,四个具名能力位、两个 core 工具、 两个 `EgressPort` 方法一并删除。趁两个 host 仓对 arise 零命中做这批破坏性变更。 ## 交付 - `ConversationInput.interaction` 改开放形状 `{kind, target_is_self, actor_id, target_id, payload}`,封闭的 `InteractionKind` Literal 删除;旧的 `target_message_id`/`emoji` 收进 `payload`。 - 新增 `ArisePorts.interaction_renderers`;core 只拥固定信封 `<interaction kind=… sender_id=… sender_name=… time=…>{host 渲染内容}</interaction>`。 - 新增 `IngressPort.to_interaction` + 一个 `@event_postprocessor` 入口, 让这条链在生产里真的可达。 - `target_is_self=True` → 统一评估管线;`False` → 转 `EnvironmentSignal` 走旁观。 - 删除 `poke`/`poke_send`/`reaction_receive`/`reaction_send` 四个能力位、 `POKE_SEND`/`REACTION_SEND` 两个 core 工具、`EgressPort.poke_send`/`reaction_send`。 ## 三处文档没说、按 grill 结论做的 1. **`actor_id`/`target_id` 照 ADR 建全五个字段。** 它们一度看着像"零读者字段" (本仓点名过的反模式),review 之后不再是:第三方那条路把它们喂给 `EnvironmentSignal.actor_id`/`subject_id`,正是 ADR-0030 决策节点名的那层复用。 2. **入口建了,不只交契约。** 新增一个 ingress port 方法同样是破坏性变更, 零 host 的今天免费,ob11 接入后就是两边协调迁移——与本票自己的立论一致。 测试适配器给了**真实**的 OB11 `PokeNotifyEvent` 实现(不是像其它几个 stub 那样 恒返回 `None`),所以整条链在测试里是真的走通的。 3. **未注册 `kind` 的行为**沿用 `tags.py` 先例:整条剔除、不留占位。 ## 两轴 review:13 条 finding 全部存活,0 条被驳回 **blocker(两轴独立撞上同一处)**:「未注册 kind 整条剔除」原先只做在渲染层, 入口不查 registry——事件照样入队、跑完两级门控、发起一次 LLM 调用,而那一轮的 context 里**一条 user 消息都没有**,模型凭空编一句发出去。空 registry 是 `ports.py` 明说的合法配置("不是错误配置"),所以这是任何没注册交互原语的部署 都会撞上的路径。与 `tags.py` 那条先例的差别在于:未注册的 inline 标签只是从 **已经决定要发的一段文本**里被抠掉,而这里被抠掉的是**触发本身的全部内容**。 闸已提到入口。 **Spec 轴抓到我写了一个可证伪的理由**:`target_is_self=False` 原先只做了"不进 反应式批次",我给的不做理由是"core 造不出 `EnvironmentSignal`,那个类型要 `plugin`/`action`"。复核实跑推翻了它——`action` 就是 `kind`,`chat_id`/`time` 都在手上,只差 `plugin` 一个 core 自选常量。这属于本仓记过七次的「给不做的分支 配一个听起来合理但错误的理由」,**第八次**。已按 ADR 实现完整路由。 其余:5 处把已删符号当现存事物引用的散文(含两处过期计数「六个裸的平台调用」, 按 issue #88/#102 确立的处置**删数字而不是改数字**)、一条承诺大于覆盖的守卫 docstring、一段被抽走后悬空的注释。 ## 有意不改,交评估侧 **戳一戳不撤销沉默窗口。** 若 `chat_id` 上有一条**不经 `_pending_flushes`** 的 待发消息正在沉默窗口里等(环境信号 tier-2 / Callback 送回 / 即刻追问三条路都是 这样),一次戳一戳不会撤掉它,于是那条和本轮回复会一前一后都发出去;换成一条 普通消息则会被 `_interrupt_silence_window_on_any_message` 撤掉。 不改的理由:CONTEXT.md 给沉默窗口写的触发条件是"窗口内这个 chat 里任何人**发言** 即撤销,补位而非抢话",戳一戳不是发言——把它算进去是扩展一条已拍板机制的触发面, 不该由实现方在本票里顺手定。理由已记在 `_enqueue_for_debounced_turn` 的 docstring 里。**这条需要评估侧决定要不要开票。** **同一形状的相邻缺口**:`_handle_environment_signal` 与 `_handle_presence_events` 同样直接 await host 回调、同样没有 try/except,而 `_apply_event_postprocessors` 把 全部 postprocessor 交给同一个 `anyio.create_task_group()`——任一子任务抛出会取消 同组其余任务(外层那个 `catch` 只记日志)。本票给自己新加的入口兜住了,那两条 既有的没动,**同样交评估侧**。 ## 验证 - 全量 **1541 passed**(新增 13),`ruff check` 干净。 - **10 项变异全部按预期变红**:去掉 `target_is_self` 半闸 / 去掉 `interaction is None` 半闸 / 未注册 kind 返回空信封 / 去掉上下文组装的 None 过滤 / 信封丢掉 kind 属性 / 去掉入口 try/except / 把 `poke_send` 无条件加回工具表 / 把 `poke` 能力位加回来 / 去掉入口的 registry 闸 / 第三方分支改回直接丢弃。 每次变异都先断言"真写进去了",还原后 md5 全部一致、哨兵残留 0。 ## 尚未做 文档同步(`docs` 分支,按规则永不推送):ADR-0030 追加"已实现"更新节、 ADR-0004 与它的矛盾、CONTEXT.md 两条术语,以及 review 补出来的 ADR-0012 两处过期计数与 ADR-0020:125 的过期先例引用。落在后续提交。 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ADR-0030 整份落地。core 不再理解 poke/reaction 这两个 QQ 概念:

- `ConversationInput.interaction` 改开放形状 `{kind, target_is_self,
  actor_id, target_id, payload}`,封闭的 `InteractionKind` Literal 删除。
- 新增 `ArisePorts.interaction_renderers`,host 按 kind 注册渲染函数;core
  只拥固定信封 `<interaction kind=… sender_id=… sender_name=… time=…>`。
  未注册 kind 整条剔除、不留占位(同 tags.py 既有先例)。
- 新增 `IngressPort.to_interaction` 与一个 `@event_postprocessor` 入口,
  让这条链在生产里真的可达;`target_is_self=False` 不进反应式批次。
- 删除四个能力位(poke/poke_send/reaction_receive/reaction_send)、两个
  core 工具(POKE_SEND/REACTION_SEND)、两个 EgressPort 方法。

趁两个 host 仓对 arise 零命中做这批破坏性变更。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
两轴 13 条 finding 全部存活(0 条被驳回),其中一条 blocker 由两轴独立撞上:

- **blocker**:「未注册 kind 整条剔除」只做在渲染层,入口不查 registry ——
  事件照样入队、跑完两级门控、发起一次 LLM 调用,而那一轮 context 里一条
  user 消息都没有,模型凭空编一句发出去。空 registry 是 ports.py 明说的合法
  配置,所以任何没注册交互原语的部署都会撞上。闸提到入口。
- **Spec 轴**:`target_is_self=False` 只做了「不进反应式批次」,没转成
  ADR-0030 指定的 `EnvironmentSignal`;而我给的不做理由(core 拿不到
  plugin/action)经复核**可证伪**——action 就是 kind,只差一个 core 自选
  常量。已按 ADR 实现,`actor_id`/`target_id` 因此有了真实读取点。
- 5 处把已删符号当现存事物引用的散文(含两处过期计数「六个裸的平台调用」,
  按仓内惯例删数字而非改数字)。
- `test_tools.py` 全集守卫的 docstring 承诺大于覆盖:漏掉 note_pending_intent
  与 pull_cross_platform_profile 两条另行注册的路,已收窄措辞并点名。
- `_handle_reactive_message` 里一段被抽走后悬空的注释。

沉默窗口不被戳一戳撤销这一项**有意不改**,理由记在 `_enqueue_for_debounced_turn`
的 docstring 里。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Yushu merged commit 938496ddfd into main 2026-08-03 05:10:50 +00:00
Yushu deleted branch feat/104-open-interaction-registry 2026-08-03 05:10:52 +00:00
Sign in to join this conversation.
No description provided.