BigPizzaV3/CodexPlusPlus · error · Error
result?.message||'Codex++ 会话读取失败'
Error message
result?.message||'Codex++ 会话读取失败'
What it means
graphFromExport() is the entry point that turns a Codex++ /api/session export into the canvas graph. It first verifies result.status === 'ok'; on any other status it throws with the backend-provided message, defaulting to the generic Chinese string 'Codex++ 会话读取失败'. This surfaces transport/backend-level failures (session read errors) distinctly from the subsequent shape checks (kind/session_id/content).
Solutions
- Verify the Codex++ backend/session API is running and the response is JSON with status:'ok' (inspect the raw result in console).
- Re-check the threadId passed to graphFromExport — refresh the session list and retry with a valid id.
- Upgrade or re-sync canvas.user.js with the backend so the expected export contract matches.
- Improve the throw to include result.status and threadId for faster diagnosis of which case fired.
Example fix
// before
if(result?.status!=='ok')throw new Error(result?.message||'Codex++ 会话读取失败');
// after
if(result?.status!=='ok')throw new Error(result?.message||`Codex++ 会话读取失败(status=${result?.status ?? '无响应'},thread=${threadId})`); Defensive patterns
Strategy: type-guard
Validate before calling
const isOkExport = (r, threadId) => r?.status === 'ok' && r?.kind === 'codex-rollout' && r?.session_id === threadId && typeof r?.content === 'string';
if (!isOkExport(result, threadId)) { view.showError('会话导出不可用,请刷新后重试'); return; } Type guard
function isValidExport(r) {
return r != null && typeof r === 'object' && r.status === 'ok' && r.kind === 'codex-rollout' && typeof r.content === 'string';
} Try / catch
try {
const graph = graphFromExport(result, threadId, annotations);
} catch (e) {
if (String(e).includes('会话读取失败')) {
console.error('session export failed', { threadId, result });
view.showError('无法读取 Codex++ 会话,请确认 Codex++ 正在运行并重试。');
} else throw e;
} Prevention
- Validate the export shape (status/kind/session_id/content) before calling graphFromExport.
- Confirm the Codex++ backend is reachable and the session API contract matches the userscript version.
- Log the raw result object when the guard fails to distinguish transport vs shape failures.
- Re-fetch/refresh the session list rather than reusing a possibly stale threadId.
When it happens
Trigger: Calling graphFromExport (via sync) with a result object whose status is 'error'/'failed'/undefined — e.g. the userscript's GM fetch to the Codex++ session API returned an error payload, timed out, or the backend failed to read the rollout file for threadId.
Common situations: Codex++ server not running or the session API path changed after an upgrade; the rollout file for the given threadId was deleted or is unreadable; response was HTML/JSON of a different shape so status is undefined; stale threadId passed after a session reset.
Related errors
- API 未能生成本批整理结果
- API 输出达到长度上限,本批未提交;请使用输出容量更大的模型
- API 未返回有效的 choices[0].message.content,请确认兼容 Chat Completions
- app-server 未连接
- 整理结果格式不完整
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19).
Data as JSON: /api/errors/68fe2c51b2fa5d6f.
Report an issue: GitHub.
Appendix: source
Thrown at tools/conversation-canvas/public/canvas.user.js:87
rows.push({...row,childCount:children.length,active:view.activePath.has(row.node.id)});
if(!collapsed.has(row.node.id))for(let i=children.length-1;i>=0;i--)stack.push({node:children[i],depth:row.depth+1});
}
return {rows,pending:view.pending};
}
function treeMarkup(nodes,currentNodeId,collapsed=new Set(),selected=null,options={}){
const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const view=visibleTreeRows(nodes,currentNodeId,collapsed),offset=options.offset||0,pendingOffset=options.pendingOffset||0;
const card=n=>`<button class="node ${selected===n.id?'selected':''} ${n.id===currentNodeId?'current':''}" data-node="${esc(n.id)}"><small>${esc(n.status)} · ${n.summaryKind==='原文摘录'?'原文摘录':'模型归纳'}${n.id===currentNodeId?' · 当前推进':''}</small><strong>${esc(n.title)}</strong><p>${esc(n.summary)}</p></button>`;
const tree=view.rows.length?`<ul class="task-tree" aria-label="模型识别的任务树">${view.rows.slice(offset,offset+100).map(({node:n,depth,childCount,active})=>`<li class="task-branch ${active?'active-path':''}" style="margin-left:${Math.min(depth,8)*14}px"><div class="tree-row">${childCount?`<button class="tree-toggle" data-collapse="${esc(n.id)}" aria-label="${collapsed.has(n.id)?'展开':'收起'} ${esc(n.title)}" aria-expanded="${!collapsed.has(n.id)}">${collapsed.has(n.id)?'▸':'▾'}</button>`:'<span class="tree-leaf" aria-hidden="true">·</span>'}${card(n)}</div>${depth>8?`<small>第 ${depth+1} 层 · 详情中可查看完整路径</small>`:''}</li>`).join('')}</ul>`:'<p class="note">点击「整理脉络」,由 Codex 识别目标、方案与多层尝试。</p>';
const pending=view.pending.length?`<details class="pending-messages" ${options.pendingOpen?'open':''}><summary>待整理消息 · ${view.pending.length} 条</summary><p class="note">任务归属将在下一次整理时识别。</p><div class="pending-list">${view.pending.slice(pendingOffset,pendingOffset+20).map(card).join('')}</div><div class="tree-actions"><button data-pending-page="${Math.max(0,pendingOffset-20)}" ${pendingOffset===0?'disabled':''}>前 20 条</button><span>${pendingOffset+1}–${Math.min(pendingOffset+20,view.pending.length)}</span><button data-pending-page="${pendingOffset+20}" ${pendingOffset+20>=view.pending.length?'disabled':''}>后 20 条</button></div></details>`:'';
return tree+pending;
}
function graphFromExport(result, threadId, annotations={nodes:[]}) {
if(result?.status!=='ok')throw new Error(result?.message||'Codex++ 会话读取失败');
if(result.kind!=='codex-rollout'||result.session_id!==threadId||typeof result.content!=='string')throw new Error('会话接口返回了不匹配的数据');
const messages=[],seen=new Set();
let declaredId=null;
// Ignore an unfinished JSONL tail; the next refresh will read the completed line.
const lines=result.content.slice(0,result.content.lastIndexOf('\n')+1).split('\n');
for(let i=0;i<lines.length;i++){
if(!lines[i].trim())continue;
let r;try{r=JSON.parse(lines[i]);}catch{continue;}
if(r.type==='session_meta')declaredId=r.payload?.id;
const m=parseMessage(r,`message-${threadId}-line-${i}`);
if(m&&!seen.has(m.id)){seen.add(m.id);messages.push(m);}
}
if(declaredId&&declaredId!==threadId)throw new Error('记录中的任务 ID 与当前任务不一致');
return {threadId,title:annotations.title||'当前对话',messages,...buildGraph(messages,annotations)};
}
function organizationPrompt(messages, requestId) {
const history=messages.map(({id,role,text})=>({id,role,text}));View on GitHub (pinned to b1ed92e5e4)