博客
通知不该只打开 App,它应该把你带回正确的 Session
推送通知不是“送到了”就算完成。对多会话 AI 编程来说,通知必须带着 server、session 和当前状态一起到达,否则它只是把中断从锁屏搬进了 App。1.6.6 修的就是这条上下文回流链路。
继续查看该栏目下的产品与工程更新。
浏览此分类我们通常把通知理解成一个送达问题:到了没有、快不快、能不能上锁屏。对远程 AI 编程来说,这个定义太浅了。
如果一个通知只能做到“把 App 打开”,那它并没有真正闭合反馈环。你仍然要自己去恢复现场:这是哪台机器?哪个会话?Claude 是在要求批准,还是已经完成?它对应的是你现在正在看的任务,还是半小时前丢到后台的另一个 session?
这不是文案问题,也不是跳转动画问题,而是一个完整的状态同步问题。
为什么这个问题在 AI 编程场景里更严重
在很多普通 App 里,一条通知通常只对应一个对象:一封邮件、一条消息、一个日历事件。你点进去后,目标实体天然是明确的。
但远程 AI 编程不是这样。
你可能同时有三个 session 在跑:
- 一个在重构某个模块
- 一个在等测试结果
- 另一个停在权限批准上
这时候,“Claude 需要你”这句话没有意义。真正有意义的是:
- 哪台 server 在发这条通知
- 哪个 session 触发了这条通知
- 这个 session 当前处在什么状态
- 用户现在是否已经在看这个 session
如果这些信息缺任何一个,通知就会退化成一个模糊提醒。它能打断你,但不能帮助你决策。
通知失效的真正方式,不是收不到,而是落错地方
很多系统把通知可靠性理解成“尽量别丢”。但在多会话工作流里,另一个同样糟糕的失败方式是:通知到了,但把你带去了错误的上下文。
这类错误比完全收不到更隐蔽,也更危险。
因为用户会以为自己已经回到了正确位置,随后在错误的 session 上继续操作。对于远程 AI 编程,这意味着你可能会:
- 在错误的仓库里回复 Claude
- 给错误的任务做审批
- 把另一个 session 的状态误判成当前任务的状态
从交互上看,这像是一次普通跳转错误;从产品上看,它破坏的是用户对整套远程工作流的信任。
要让通知真正可用,上下文必须贯穿整条链路
一条有用的通知,不只是一个 APNs payload。它必须穿过完整链路,而且在每一段都不能丢失身份信息:
- 服务端 hook 事件产生时,要知道触发它的是哪个
tmux_session - 服务端广播通知时,要把
session信息一起带出去 - push relay 去重时,不能只按项目名去重,否则不同 session 会互相吞掉
- App 冷启动时,要能把 push 里的 server identity 映射回本地保存的 server
- 最终导航时,要回到正确的 server,再选中正确的 session,而不是落到默认页
只要其中一段做成“尽量猜”,最终结果就会不稳定。
这也是为什么 1.6.6 里和通知相关的改动,看起来分散在 iOS、Mac、server、push relay 几个位置,但本质上它们都在修同一个问题:让通知成为一个可执行的上下文入口,而不是一个泛泛的提醒入口。
旧方案到底是怎么失效的
如果把这类问题抽象一下,旧方案的失败模式主要有四种。
1. 事件源有 session,通知里却没有 session
服务端 hook 明明知道事件来自某个 tmux_session,但通知广播只发了“有一条通知”,没有把 session 一起传下去。到了客户端,系统就只能退化成“先回当前页再说”。
这会制造一种很隐蔽的假象:通知点开后页面确实跳了,看起来像“功能存在”,但其实落点经常是错的。
2. 热启动可用,不代表冷启动可用
很多通知跳转在前台路径下都没问题,真正容易坏的是冷启动路径。因为这时 App 还没恢复完成,server 列表、导航状态、本地缓存对象都可能还没准备好。
如果这时候直接拿 push payload 去驱动页面跳转,就很容易出现两种结果:
- 页面没跳
- 页面跳了,但跳到了错误的 server 或 session
3. 去重逻辑把两个 session 错当成一个事件
如果 push relay 的去重键只按 server + project + event 设计,而没有纳入 session,那么同一项目下两个 session 先后触发事件时,后一个通知就可能被前一个吞掉。
这类问题不会直接崩溃,但会表现成最难排查的用户反馈:“明明完成了,为什么没收到通知?”
4. 客户端把“当前正在看的 session”误当成“通知来源 session”
这是一个很典型的建模错误。当前 UI 聚焦在哪个 session,和刚刚发出通知的是哪个 session,本来就是两个不同概念。如果状态层没把它们拆开,跳错几乎是必然结果。
1.6.6 实际修的不是一个按钮,而是一条路径
这次改动里,真正重要的不是“支持点击通知跳转”这句话,而是下面这些约束终于被连起来了:
1. 通知携带 session 身份
以前通知链路里存在一个常见错误:事件在服务端产生时明明属于 session B,但客户端收到通知后却只能知道“有一条通知”,最后默认落到当前活跃的 session A。
1.6.6 把 session 身份补到了通知广播和 push 载荷里。这样通知不再只是“Claude 有事找你”,而是“repo-x / session-y 有事找你”。
2. 冷启动不再丢 deep link
热启动跳转相对简单,冷启动才是最容易出问题的地方。App 尚未完成初始化,server 列表未必已经载入,本地保存的 server ID 也未必和 push payload 使用的是同一套标识。
这次修复的关键点不是界面,而是 ID 映射:push 带来的是 server 端 identity,App 里保存的是本地对象 ID。只有把这两套身份对上,冷启动跳转才不是“看起来支持”,而是真的能落到正确目标。
换句话说,问题不在“页面怎么跳”,而在“系统怎么确认这就是同一台 server”。
3. 推送投递不再把 session 当作可忽略细节
如果去重键里不包含 session,同一项目下两个不同会话在短时间内触发通知,后一个就可能被前一个吞掉。用户看到的表现不是“去重过度”,而是“为什么明明完成了却没通知我”。
这说明在 AI 工作流里,session 不是附属字段,而是通知语义的一部分。
4. 本地通知、远程 push 和页面导航终于开始说同一种语义
一条通知链路里,最怕的是三层各说各话:
-
服务端说“这是 hook 事件”
-
push 服务说“这是 project 层通知”
-
客户端说“那我先回当前页”
1.6.6 的重要价值在于,这三层开始围绕同一个目标对齐:把用户带回生成该事件的 server 和 session。
这为什么是产品问题,而不只是工程问题
看起来我们一直在讨论 ID、payload 和 deep link,但这背后其实是一个更大的产品判断:
通知是否应该被设计成“把用户带回正确决策现场”的机制。
如果答案是是,那么通知就不能只追求送达率。它必须同时满足三个条件:
- 能到
- 能落到对的地方
- 到了之后用户能立刻采取下一步动作
从这个标准看,很多产品里的通知其实都只做到第一步。
用户实际能感受到的变化是什么
如果完全不看内部实现,只从日常使用角度看,1.6.6 至少改善了四件事:
- 点开通知后,更少跳错到其他 session
- 从锁屏或已终止状态唤起 App 时,更少丢上下文
- 弱网下通知更不容易因为一次瞬时失败直接丢失
- Live Activity 在异常路径里的状态比以前更可信
这四件事单看都不 flashy,但它们共同决定了一件更重要的事:你敢不敢真的同时挂多个 AI 会话在后台跑。
可靠也意味着诚实描述边界
这次版本里,我们还重写了 Live Activity 的清理路径:重启时回收陈旧 activity、清理孤儿引用、降低临时断线后错误显示“已完成”的概率。
但有一个边界场景我们没有在发布文案里假装它已经解决:用户强制退出 iOS App 后,Live Activity 仍可能停留到下次启动或系统超时。
这类问题如果被写成“已彻底修复”,短期看文案更漂亮,长期看只会伤害可信度。对开发者工具尤其如此。因为你卖的不是情绪,而是可预期性。
真正的目标不是多发通知,而是降低恢复上下文的成本
优秀的远程 AI 产品,不应该要求用户每次收到通知后都重新理解一次“刚才发生了什么”。
通知真正应该做的是:把恢复上下文这件事提前做掉。
对远程 AI 编程来说,送达只是门票。能不能带着完整上下文一起到达,才决定了这条通知到底是“有用的输入”,还是“另一次打断”。
1.6.6 没有发明通知,也没有发明多会话。它做的是更基础、也更重要的一件事:让这两者终于能正确协同工作。