{"record":{"id":"97093bb0ce0eeaad","repo":"BigPizzaV3/CodexPlusPlus","slug":"codex-97093b","errorCode":null,"errorMessage":"Codex++ 会话读取失败","messagePattern":"Codex\\+\\+ 会话读取失败","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tools/conversation-canvas/public/canvas.user.js","lineNumber":1147,"sourceCode":"    }\n    if(!target){$('jump-status').textContent=({revealed:'已展开所属轮次，但该消息仍被折叠或未显示。可先查看对应原文。',unavailable:'当前页面的历史定位尚未就绪。可先查看对应原文。','missing-turn':'该来源缺少轮次信息。可先查看对应原文。',failed:'原聊天轮次展开失败。可先查看对应原文。'})[navigationState];return;}\n    closePanel();\n    target.scrollIntoView({behavior:'smooth',block:'center'});\n    target.animate([{outline:'2px solid #888',backgroundColor:'#8882'},{outline:'2px solid transparent',backgroundColor:'transparent'}],{duration:2000});\n    $('jump-status').textContent='正在定位原聊天消息…';\n    setTimeout(()=>{if(disposed||currentId()!==threadId)return;const r=target.getBoundingClientRect();$('jump-status').textContent=target.isConnected&&r.height>0&&r.bottom>0&&r.top<innerHeight?'已定位并高亮原聊天消息。':'消息已找到，滚动尚未完成。';},700);\n  }\n  async function readCanvasSource(id,signal,progress){\n    if(!nativeReadTasks.has(id)){\n      let result;\n      try{\n        if(typeof window.__codexSessionDeleteBridge!=='function')throw Error('Codex++ 会话接口尚未连接，请稍后点击刷新');\n        let timer;\n        try{result=await Promise.race([window.__codexSessionDeleteBridge('/session/export',{session_id:id}),new Promise((_,reject)=>{timer=setTimeout(()=>reject(Error('会话读取超时，请重试')),15000);})]);}\n        finally{clearTimeout(timer);}\n      }catch(error){if(!isExportSizeError(error.message))throw error;result={message:error.message};}\n      if(result?.status==='ok')return result;\n      if(!isExportSizeError(result?.message))throw Error(result?.message||'Codex++ 会话读取失败');\n      nativeReadTasks.add(id);\n    }\n    signal.throwIfAborted();progress('会话较大，正在改用原生分页读取…');\n    const {manager}=await nativeContext(id);\n    const messages=await readNativeHistory((method,params)=>manager.sendRequest(method,params,{priority:'background',timeoutMs:15000}),id,historyCache,progress,signal);\n    const signature=[];for(const message of messages){signal.throwIfAborted();signature.push(message.id+':'+await messageDigest(message));}\n    return {status:'ok',kind:'canvas-messages',session_id:id,messages,content:signature.join('|')};\n  }\n  async function sync(force=false){\n    const id=currentId();\n    if(!id){historyAbort?.abort();requestSequence++;active='';pending=null;data=null;revision='';taskCanvas.context('');$('pending-tray').replaceChildren();$('transcript').replaceChildren();$('details').hidden=true;status('请打开具体任务。');return;}\n    if(id!==active){\n      historyAbort?.abort();taskCanvas.context(id);pendingOffset=0;pendingOpen=false;sourceState={page:0};collapsedByThread.set(active,collapsedNodes);collapsedNodes=collapsedByThread.get(id)||new Set();active=id;organizationStatus(organizing===id?'GPT-5.6 Luna · 中 · 正在后台整理…':'');requestSequence++;pending=null;data=null;revision='';selected=null;lastRead=0;\n      $('pending-tray').replaceChildren();$('transcript').replaceChildren();$('details').hidden=true;$('subtitle').textContent='';$('title').textContent='当前对话';tab('map');\n    }\n    if(pending===id||(!force&&Date.now()-lastRead<5000))return;\n    const seq=++requestSequence;pending=id;lastRead=Date.now();if(!data)status('正在读取当前对话…');\n    const abort=new AbortController();historyAbort=abort;","sourceCodeStart":1129,"sourceCodeEnd":1165,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/tools/conversation-canvas/public/canvas.user.js#L1129-L1165","documentation":"After the bridge call to /session/export returns, readCanvasSource requires result.status==='ok'. If the export failed with any message that is not the recognized export-size-limit error, it throws the bridge's message or the generic 'Codex++ 会话读取失败' fallback.","triggerScenarios":"__codexSessionDeleteBridge('/session/export',{session_id:id}) resolves with a payload lacking status:'ok' and whose message is not matched by isExportSizeError — e.g. backend returned an error object, unknown failure, or an empty/undefined result.","commonSituations":"Session id no longer exists on the backend; Codex backend API changed shape after an update; session export endpoint returned an unexpected error; result is undefined because the bridge swallowed the failure.","solutions":["Click refresh to retry the export; transient backend failures often clear","Confirm the session id is valid and the session still exists in Codex","Check the Codex++ backend logs for the /session/export failure reason","Update Codex++ if the Codex app was recently upgraded (response shape drift)"],"exampleFix":"// before\nif(!isExportSizeError(result?.message))throw Error(result?.message||'Codex++ 会话读取失败');\n// after\nif(!result)throw Error('Codex++ 会话读取失败：桥接返回为空');\nif(!isExportSizeError(result?.message))throw Error(result?.message||'Codex++ 会话读取失败');","handlingStrategy":"try-catch","validationCode":"if(!result||result.status!=='ok'&&!isExportSizeError(result?.message)){/* treat as failure before proceeding */}","typeGuard":"const isOkExport=(r)=>!!r&&r.status==='ok'&&Array.isArray(r.messages||r.data);","tryCatchPattern":"try{const r=await bridge('/session/export',{session_id:id});}catch(e){if(isExportSizeError(e.message)){/*fall back to native paging*/}else{showToast('会话读取失败，请重试');}}","preventionTips":["Always check status==='ok' before consuming export payloads","Log the raw bridge response for diagnosis","Handle the export-size case separately via isExportSizeError","Retry transient failures with backoff"],"tags":["session-export","bridge","api-error"],"backgroundTag":"api-error-response","analyzedSha":"b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6","analyzedAt":"2026-09-19T23:35:21.129Z","contentChangedAt":"2026-09-19T23:35:21.129Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}