返回列表

博客

通知不该只打开 App,它应该把你带回正确的 Session

推送通知不是“送到了”就算完成。对多会话 AI 编程来说,通知必须带着 server、session 和当前状态一起到达,否则它只是把中断从锁屏搬进了 App。1.6.6 修的就是这条上下文回流链路。

发布于 标签: product / workflow / notifications / engineering
博客

继续查看该栏目下的产品与工程更新。

浏览此分类

我们通常把通知理解成一个送达问题:到了没有、快不快、能不能上锁屏。对远程 AI 编程来说,这个定义太浅了。

如果一个通知只能做到“把 App 打开”,那它并没有真正闭合反馈环。你仍然要自己去恢复现场:这是哪台机器?哪个会话?Claude 是在要求批准,还是已经完成?它对应的是你现在正在看的任务,还是半小时前丢到后台的另一个 session?

这不是文案问题,也不是跳转动画问题,而是一个完整的状态同步问题。

为什么这个问题在 AI 编程场景里更严重

在很多普通 App 里,一条通知通常只对应一个对象:一封邮件、一条消息、一个日历事件。你点进去后,目标实体天然是明确的。

但远程 AI 编程不是这样。

你可能同时有三个 session 在跑:

  • 一个在重构某个模块
  • 一个在等测试结果
  • 另一个停在权限批准上

这时候,“Claude 需要你”这句话没有意义。真正有意义的是:

  • 哪台 server 在发这条通知
  • 哪个 session 触发了这条通知
  • 这个 session 当前处在什么状态
  • 用户现在是否已经在看这个 session

如果这些信息缺任何一个,通知就会退化成一个模糊提醒。它能打断你,但不能帮助你决策。

通知失效的真正方式,不是收不到,而是落错地方

很多系统把通知可靠性理解成“尽量别丢”。但在多会话工作流里,另一个同样糟糕的失败方式是:通知到了,但把你带去了错误的上下文。

这类错误比完全收不到更隐蔽,也更危险。

因为用户会以为自己已经回到了正确位置,随后在错误的 session 上继续操作。对于远程 AI 编程,这意味着你可能会:

  • 在错误的仓库里回复 Claude
  • 给错误的任务做审批
  • 把另一个 session 的状态误判成当前任务的状态

从交互上看,这像是一次普通跳转错误;从产品上看,它破坏的是用户对整套远程工作流的信任。

要让通知真正可用,上下文必须贯穿整条链路

一条有用的通知,不只是一个 APNs payload。它必须穿过完整链路,而且在每一段都不能丢失身份信息:

  1. 服务端 hook 事件产生时,要知道触发它的是哪个 tmux_session
  2. 服务端广播通知时,要把 session 信息一起带出去
  3. push relay 去重时,不能只按项目名去重,否则不同 session 会互相吞掉
  4. App 冷启动时,要能把 push 里的 server identity 映射回本地保存的 server
  5. 最终导航时,要回到正确的 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 有事找你”。

热启动跳转相对简单,冷启动才是最容易出问题的地方。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 至少改善了四件事:

  1. 点开通知后,更少跳错到其他 session
  2. 从锁屏或已终止状态唤起 App 时,更少丢上下文
  3. 弱网下通知更不容易因为一次瞬时失败直接丢失
  4. Live Activity 在异常路径里的状态比以前更可信

这四件事单看都不 flashy,但它们共同决定了一件更重要的事:你敢不敢真的同时挂多个 AI 会话在后台跑。

可靠也意味着诚实描述边界

这次版本里,我们还重写了 Live Activity 的清理路径:重启时回收陈旧 activity、清理孤儿引用、降低临时断线后错误显示“已完成”的概率。

但有一个边界场景我们没有在发布文案里假装它已经解决:用户强制退出 iOS App 后,Live Activity 仍可能停留到下次启动或系统超时。

这类问题如果被写成“已彻底修复”,短期看文案更漂亮,长期看只会伤害可信度。对开发者工具尤其如此。因为你卖的不是情绪,而是可预期性。

真正的目标不是多发通知,而是降低恢复上下文的成本

优秀的远程 AI 产品,不应该要求用户每次收到通知后都重新理解一次“刚才发生了什么”。

通知真正应该做的是:把恢复上下文这件事提前做掉。

对远程 AI 编程来说,送达只是门票。能不能带着完整上下文一起到达,才决定了这条通知到底是“有用的输入”,还是“另一次打断”。

1.6.6 没有发明通知,也没有发明多会话。它做的是更基础、也更重要的一件事:让这两者终于能正确协同工作。

立即体验 Tactic Remote

用手机远程控制编程 Agent

连接 Mac、Windows 或 Linux 电脑上的 Claude Code、Codex 等 Agent,在 iPhone 或 iPad 上查看进展并发送下一条要求。