# 会话查询与实时订阅契约 ## 1. 适用范围与触发条件 浏览器打开会话、查看更早消息、索引用户目录、定位消息和恢复运行视图时适用。 已有实现把这些读取绑定到 WebSocket 的 OPEN、鉴权和运行绑定;假 OPEN 时请求可能 丢在旧连接里,直到45秒超时。历史查询必须有独立 HTTP 通道,重连只恢复实时订阅。 这项代码事实不能用于断言每次现场故障都来自同一原因。 ## 2. 接口与函数签名 ```text GET /api/sessions/:id?requestId= GET /api/sessions/:id/history?before=<非负整数>&requestId= Authorization: Bearer <已有登录令牌> handleSessionQueryApi(req, res, url, encodedId, historyPage = false) resolveSessionHistory(session, { count, before, persistedOnly: true }) loadSession(id, { metadata: true, throwOnError: true }) WS → { type: 'subscribe_session', sessionId, requestId, historyRevision } WS ← { type: 'session_subscribed', sessionId, requestId, historyRevision, historyChanged, isRunning } ``` ## 3. 请求、响应与并发约束 - 首屏返回 `type='session_info'`、最近12条、`historyPending=false` 和会话元数据。 分页返回 `type='session_history_chunk'`,向前最多24条,`remaining=0`。 - 两者都提供 `sessionId/requestId/messages/historyTotal/historyBaseIndex/historyCursor/` `historyAvailable/historySource/historyRevision`;`requestId` 最多160字符,浏览器生成 有界且唯一的代次标识,成功响应必须同时匹配会话和请求。 - 首屏的 `runtime` 是完整 `resume_generating` 快照或 `null`,即使没有 WebSocket 也能显示已产生输出。停止运行但尚未归档的保留态显示后须收束生成状态。 - HTTP 只读取 cc-web 自身已保存记录;首屏不扫描原生日志、不协调跨会话、不清未读。 归档按块有界读取;旧快照保留来源待核实状态。磁盘和 JSON 处理仍在同一服务进程, 传输拆分不代表所有同步 IO 已异步化。 - 订阅校验元数据、绑定实时输出、清未读并冲刷已有跨会话回复,不发送历史消息。 比较清未读前的归档修订;有差异时由浏览器再通过 HTTP 对账。确认后返回清未读 产生的新修订,避免元数据保存导致重复对账。 - 首屏、正文分页、目录分页和消息定位均不检查 WebSocket 是否已连接或鉴权。 WebSocket 关闭不能取消 HTTP 读取、清除分页状态或结束首屏加载。 - 新选择、同会话新代次和退出视图取消旧 HTTP;控制器、视图代次和活动分页对象 三重隔离迟到结果。错误恢复保留已有视图和分页游标,取消后清除加载标志。 - `session_subscribed` 和带请求标识的运行快照必须属于当前订阅或视图。 订阅触发的历史对账保留实时输出,不再应用较早的 HTTP runtime,也不重复订阅。 - 若查询期间已收到该会话的 `done`,弃用较早快照并重新查询终态;不能让完成后的 页面被旧 `isRunning=true` 快照覆盖。 - HTTP 45秒硬超时只限制真实请求;每个成功、失败或取消出口都清理计时器。 `Server-Timing` 和 `session_http_query` 记录请求、阶段、状态、数量和耗时,不记录正文。 ## 4. 校验与错误矩阵 | 条件 | HTTP / 订阅行为 | |---|---| | 缺失或无效 Bearer | 401 `not_authenticated` | | URI 解码失败或 ID 含字母数字、连字符之外字符 | 400 `history_invalid_id`;不得清洗后别名到另一会话 | | 游标为负数、小数或非安全整数 | 400 `history_invalid_cursor` | | 会话确实缺失 | 404 `session_not_found` | | 元数据、清单或历史块读取失败 | 500 `history_read_failed`,禁止吞成404 | | 实时绑定失败 | WS `session_subscribe_failed`,历史继续可读 | | 新选择取消、迟到响应或不匹配订阅 | 不修改新视图及其加载状态 | 所有 HTTP 错误携带 `type='error'`、`code/message/sessionId/requestId`。未完成身份 校验或 URI 解码时 `sessionId` 可以为空,不泄露磁盘路径及消息内容。 ## 5. 正常、基本与失败场景 - 正常:57条归档关闭 WS 后首屏12条、继续分页24条和21条,按原顺序且无重复。 - 基本:运行中会话在假 OPEN 时仍显示历史和完整运行输出,实时订阅独立恢复。 - 交错:HTTP 读完后任务完成,订阅修订变化触发 HTTP 对账,最终回复显示且无生成占位。 - 失败:A 响应晚于 B,A 的正文和运行态都不能覆盖 B;坏历史返回500并保留当前视图。 ## 6. 必需回归与断言 - `scripts/session-http-load-frontend-unit.js` 执行生产请求函数:无 WS 读取、真实 abort、 忽略取消后依然到达的响应、同会话代次、分页取消、完成交错、500和超时计时器释放。 - `node scripts/regression.js --target session-http-load`:隔离真实服务,断 WS 后查询、 首屏/分页、鉴权、坏 ID/游标、损坏清单/块、独立订阅、修订变化和完整运行快照。 - `node scripts/session-http-load-regression.js --browser`:真实 Firefox 经转发层测试 假 OPEN、读取途中断 WS、完全断连后切换、分页、迟到 A 响应、完成间隙及历史500。 断言浏览器不发送 `load_session/load_history_page/resume_session` 查询历史。 - 外部 CDN 不稳定时可设置 `CC_WEB_BROWSER_CDN_DIR`,使用与页面相同版本的原始 `marked.min.js/highlight.min.js/mermaid.min.js/atom-one-dark.min.css` 本地夹具;仅转发层 改写这些静态 URL,生产请求函数与渲染器保持原样,避免把登录前静态资源超时当作切换失败。 - 所有数据、原生目录和 Codex home 锁路径必须隔离;不修改 HOME/CODEX_HOME 或生产锁。 单次测试最多60秒,不能用生产会话的读写代替隔离验证。 ## 7. 错误与正确示例 ```js // 错误:已有令牌也必须先等长连接;假 OPEN 时只能等待业务超时。 if (ws.readyState === 1 && wsAuthenticated) ws.send(JSON.stringify({ type: 'load_session', sessionId })); // 正确:历史独立读取,成功应用后再接入实时订阅。 const snapshot = await fetchSessionQuery(sessionId, requestId, { signal }); handleServerMessage(snapshot); requestSessionResume(sessionId); ``` 生产函数还必须执行控制器身份、请求代次和完成事件版本校验,不能仅复制此示意代码。