Skip to content

fix(gateway): 重新接入后恢复已有会话 - #3046

Merged
BigPizzaV3 merged 1 commit into
BigPizzaV3:mainfrom
130040167:codex/gateway-reconnect-standalone-20261111
Nov 12, 2026
Merged

BigPizzaV3 merged 1 commit into
BigPizzaV3:mainfrom
130040167:codex/gateway-reconnect-standalone-20261111

Conversation

@130040167

@130040167 130040167 commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

问题

外部工具改写 Codex 配置后,即使重新接入 Codex++ 网关,已经加载的会话仍可能使用旧连接,后续消息可能继续发往旧端点。

方案

  • 在网关连接配置变化或用户明确重新接入时发布连接代际;需要本地网关的操作在 helper 启动成功后发布。
  • 页面加载时建立连接代际基线;同一供应商的已落盘会话在确认代际变化后,于下次发送前尝试卸载旧线程并按当前配置恢复,保留会话、模型和推理档位。
  • 对能够观察到的本地 thread/start 和预热请求记录创建时的代际,避免新会话在代际未再变化时被重复恢复。临时会话直接放行,不分页、不卸载和不恢复,并缓存类型以避免重复检查。
  • 活动回复继续运行;代际读取失败时放行并在后续发送时再检查,其他只读预检失败或供应商不一致时提示可能沿用旧连接,并将发送交给原生会话。若已有恢复正在处理同一会话,或卸载、恢复请求可能已经发出,则返回错误,避免假定连接仍可用。
  • 保留原生客户端的订阅缓存,只解除服务端订阅,并与现有上下文刷新协调;不接管或重放原生队列。跨供应商的会话迁移由 fix: preserve local chat continuation across provider switches #3005 处理。

验证

已知限制

新版 Codex 可在宿主进程中预热线程,随后由 startConversation 取用。页面未观察到创建请求时无法可靠确认创建代际:这种新会话在页面基线与当前代际不同时,第二条消息仍可能发生一次多余恢复。不能把发送时的代际直接当成创建代际,因为预热线程可能来自旧连接。

若页面启动时的基线读取失败,下一次读取成功之前发生的代际变化无法恢复出来;恢复期间旧线程仍被占用时可能需要稍后重试。重新接入前已打开但尚未发送过消息的空会话,以及已经由原生服务派发的队列,未保证立即切到新端点。真实供应商和官方登录尚未验收。

@BigPizzaV3

Copy link
Copy Markdown
Owner

感谢提交!问题定位很清楚,Rust 侧只在连接字段变化时发布代际、模型目录刷新不会误触发,这部分做得很好,测试也覆盖得比较全。我们在独立工作树重新组装了注入产物,和分片一致;gateway_apply / bridge_routes 定向测试(111 项)也都通过了。

合入前有几处想请你调整一下:

  1. 读取代际失败时请降级,不要拦截发送(30-service-tier.js 约 256–259 行)。现在网关用户每次 turn/start 都要先在 2 秒内读到 /gateway/connection-generation,超时或返回失败就直接拒绝这条消息。桥接偶尔变慢时,用户会在连接根本没变的情况下发不出消息。这也和同文件 readCodexAppServerPreparation 注释里「桥接无回包时及时降级」的约定不一致。建议失败时放行原生发送,下次发送再检查。
  2. 请避免在代际没变时重载会话(约 274、288 行)。state.generation 初始是 undefined,所以每次页面加载后,每个已有会话的首条消息都会走一次 unsubscribe → 等待卸载(最长 70 秒)→ resume,没遇到外部改配置的用户也要承担这份延迟和「会话仍被占用」的风险。70-model-catalog 那条没传 client 的路径只能轮询等自然卸载,影响更明显。能否在页面加载时读一次代际作为基线,或者按线程把已应用的代际存进 sessionStorage,只在代际真正变化时才重载?
  3. 91-extension-rpc.js 的 prepareGateway:以前没开拓展钩子和上下文刷新的网关用户,现在每次本地发送都会经过帧解码 / 重编码,并依赖 codexPlusAppServerClient("local")。它定位失败时会抛错,导致发送被拒。这里也请改成失败时降级、原样放行帧。
  4. 约 381–387 行:当 effort 为 null、turn 没带 collaborationMode.mode,且 thread/resume 响应里没有 collaborationMode 时,collaborationMode.settings 会抛 TypeError。请加一下空值保护,或者确认 resume 响应里一定有这个字段。
  5. 麻烦补两个用例:「读取代际超时 / 失败时消息仍能发出」「页面首次加载、代际未变时不触发 resume」。

另外 Linux job 还在跑,等它通过后我们再看一轮。辛苦了!

@BigPizzaV3

Copy link
Copy Markdown
Owner

感谢更新!这一轮我们在独立工作树复核过了:注入产物与分片一致,前端相关 393 项和 Rust 的 cdp_bridge / gateway_apply / bridge_routes 280 项都通过了。读取代际失败降级、页面基线、prepareGateway 降级、collaborationMode 空值保护和两个用例都已经到位,辛苦了。

还剩一个小场景想请你再看一下:页面加载后连接代际发生过变化(比如基线 one → two),之后新建的会话首条消息因为还没有历史(turns.data.length === 0)提前返回,state.generation 仍停在页面基线 one;等第二条消息时会被判定为「代际已变化」,走一次 unsubscribe → resume。这个会话本来就是在新连接下创建的,这次重载是多余的。用 gateway-reconnect.test.ts 现有的 runtime 夹具可以复现:第二条消息依次调用了 thread/read、config/read、thread/turns/list、thread/unsubscribe、thread/read、thread/resume。

建议:没有历史这条早退路径,如果能确认线程是在当前代际下创建的(例如在 thread/start 时按线程记下当时的代际),就把 state.generation 记成当前代际;ephemeral 早退那里也可以一并记下,避免之后每条消息都重复 thread/read。另外也想请你补一个用例:ephemeral(临时侧聊)线程在代际变化后发送,不调用 thread/turns/list、thread/unsubscribe、thread/resume,消息正常发出。改完我们再看一轮,谢谢!

@130040167

Copy link
Copy Markdown
Contributor Author

针对新建会话和临时侧聊两点,已在 e52d6f59 上复核:

  1. 本地 thread/start(包括页面可观察到的预热请求)会记录创建代际。能确认会话是在当前连接代际创建的情况下,首条和第二条消息都不会为这次变化额外触发 unsubscribe → resume。如果代际在创建请求期间又变了,后续仍可恢复;没有历史本身不作为「已接入当前连接」的证据。
  2. 代际变化后,临时侧聊仍交给原生发送,不调用 thread/turns/list、thread/unsubscribe 或 thread/resume;识别到临时会话后也不会每次重复读取线程。已补回归用例。

还有一个已写入描述的边界:新版 Codex 可在宿主进程预热线程,再由页面取用。页面没看到创建请求时,无法区分是旧连接的预热线程,还是当前连接新建的线程,这时第二条消息仍可能多做一次恢复。直接用发送时的代际会漏掉旧连接,因此没有做这个推断。

本次重跑网关和插件适配定向测试,51 项通过;注入产物与分片一致,当前提交的 Windows、Linux、macOS CI 均通过。真实供应商场景尚未验收。

@BigPizzaV3

Copy link
Copy Markdown
Owner

感谢!两点都核对过了:本地 thread/start 记录创建代际,新会话第二条消息不再多做一次恢复;代际变化后临时侧聊直接交给原生发送,也不再重复读线程。宿主预热线程那个边界写进描述是对的。网关相关前端测试 67 项本地通过,注入产物与分片一致。

这一轮已经可以合,只差一步:刚合入的 #3070 也升了 codexAppServerModelRequestPatchVersion(37 → 38),和这里在 00-prelude.js、renderer-inject.js、manifest.json、tests/all/cdp_bridge.rs 冲突。麻烦 rebase 到最新 main,把版本再往上升一位、重新组装注入产物,CI 过了我们就合入。

@130040167
130040167 force-pushed the codex/gateway-reconnect-standalone-20261111 branch from e52d6f5 to a114d0c Compare November 12, 2026 13:36
@130040167
130040167 force-pushed the codex/gateway-reconnect-standalone-20261111 branch from a114d0c to 41f2dfa Compare November 12, 2026 13:55
@130040167

Copy link
Copy Markdown
Contributor Author

已按这轮意见变基到 9b22eebd,当前提交 41f2dfab。请求包装版本从本 PR 的 41 升到 42,重新组装了 43 个注入分片,产物一致。

回归覆盖了三条路径:旧版 ephemeral 侧聊在代际变化后仍直接发送(#3051);新版侧聊没有历史时不卸载、不恢复,首轮先发,有历史后才在同一线程重新接入(#3062);实际上下文限制不变的账号切换继续走原生发送(#3070)。

完整前端 1,977 项通过、2 项跳过,网关、桥接、注入和插件适配相关 Rust 用例 76 项通过。当前提交的三平台 CI 均通过,当前对 main 无冲突。宿主预热线程的创建代际不可见时仍按 PR 描述中的边界处理。

@BigPizzaV3
BigPizzaV3 merged commit 3d41192 into BigPizzaV3:main Nov 12, 2026
3 checks passed
@BigPizzaV3

Copy link
Copy Markdown
Owner

感谢!rebase 后注入产物与分片一致,前端全量和三平台 CI 都通过,#3051 / #3062 / #3070 三条路径的回归也补上了。已合入 main,随下个版本发布。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants