手机休眠时保持聊天在线
Mythex 的代理可以就一个请求工作好几分钟,而你很可能是用手机在看。手机会锁屏、切换应用、更换网络。两个 bug 让这种情况出了问题。一个是聊天把每一个「页面重新可见」的信号都当作连接已断开的证据,于是不停循环,最后停在「正在连接…」。另一个是手机在新聊天的第一条回复期间休眠,醒来后只看到回复的前几块内容,直到手动刷新。两个问题现在都已修复。
聊天如何跟随正在运行的回合
代理工作时,聊天会与我们的服务器保持一条流式连接:每个步骤(一个思考、一次工具调用、一段回复内容)都以事件的形式到达。如果流断开,聊天会重新连接,服务器则从一个保存最近 120 秒事件的缓冲区中重放事件。回合结束时,聊天还会向服务器请求已保存的历史记录,并将它与显示的内容进行核对。最后这一步可以捕获流漏掉的内容。
为了在没有任何事件发生时区分连接是否正常,服务器每 15 秒会发送一个小的「ping」。
Bug 一:唤醒时杀掉了一条正常的流
浏览器会在页面重新可见时通知页面,例如切换标签页之后,或屏幕解锁之后。我们把这当作检查连接的契机,实际上是在假设流在隐藏期间已经断开,于是中止流并重新连接。
在手机和嵌入式浏览器中,这些事件触发的频率远远高于流真正断开的频率:每次切换标签页、每次解锁,而在某些嵌入式浏览器中甚至每隔几秒就会触发一次。每一次都会中止一条原本正常的流。聊天重新连接、重放、又被中止,最后停在「正在连接…」。然后它又会提示该回合已经结束,而代理其实仍在工作。
修复:根据流是否仍在通信来判断
聊天现在会记录每个项目的流最后一次收到任何数据的时间,包括事件和每 15 秒的 ping。「重新可见」的信号只会中止已经沉默 25 秒的流:这比 ping 间隔加上一次较慢的网络往返时间还要长,因此正常的流永远不会符合条件。测试会检查:一个 ping 也算作存活的迹象,而刚刚发过 ping 的流不会被视为已断开。
从中得到的规则是:页面的可见性说明的是页面的状态,而不是网络连接的状态。应该去询问连接本身。
Bug 二:手机唤醒后只看到半条回复
这个问题我们在 iOS 模拟器中每次都能复现:
- 开始一个新聊天,并发送一个需要较长时间的请求。
- 让手机在构建期间休眠。
- 等回合结束、两分钟的重放缓冲区已过期后再回来。
聊天会显示回复的前几块内容,并标记为已完成,而且会一直保持这样,直到你刷新页面。
原因
新聊天在第一条消息创建它之前,并没有线程(即保存的对话)。这一创建发生在发送函数内部,而发送函数在此之前就已经捕获了对账步骤,而那个步骤仍然认为没有线程。因此,在新聊天的第一个回合结束时,对账步骤看到没有线程,就悄悄返回,而不去请求任何内容。
通常没有人注意到,因为流已经把整个回合都送达了。但是,如果手机在回合进行期间休眠,并在 120 秒的重放缓冲区过期之后才回来,它就无法从流中获取缺失的事件,而本应填补这一空缺的对账步骤却从未运行过。
修复
- 发送时告知对账步骤该回合属于哪个线程,因此新聊天的第一个回合会像其他回合一样被对账。如果在答案返回时你已经打开了另一个聊天,结果会被丢弃,而不是显示在错误的位置。
- 对账失败会重试。 手机在网络不佳的情况下唤醒时,可能会发现回合已经结束,但获取它却失败了。现在这种情况会被记住,并且在标签页再次可见或网络恢复时,聊天会再次请求。
- 「Worked for」如实显示。 每条回复都会显示代理工作了多长时间。如果标签页从未收到服务器的回合结束消息,它以前会用自己的时钟补上这个时间,而该时钟从点击发送一直计时到手机唤醒,中间离开的时间也被算了进去。现在这个时长会被标记为估计值,并在历史记录到达后替换为存储的实际时长。
我们从中得到的经验
- 在人们实际使用的设备上,以他们实际使用的方式进行测试。 两个 bug 都藏在「一个正常工作的流」与「一部放在口袋里的手机」之间的缝隙中。
- 从不运行的兜底机制,不算兜底。 对账步骤本来就是为了捕获正是这种情况而存在的,却被悄悄跳过了。
- 恢复操作也要重试。 手机唤醒的那一刻,往往也是它的连接最糟糕的时候。
- 不要把猜测当作事实展示。 用错误时钟算出的时长,比没有时长还糟糕;要么注明,要么替换。
这次工作还整理了你在回合运行时会看到的一些细节:shell 步骤以完成工作的命令作为标签,而不是 cd;聊天中的截图保持页面的左上角并适应列宽;新项目无需刷新即可获得其自动生成的名称。
如果你要为手机构建自己的应用,我们的指南如何构建适合手机的网络应用和原生应用与渐进式网络应用涵盖了基础知识。关于聊天本身的更多信息,请参阅聊天文档。