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

  1. Verify the Codex++ backend/session API is running and the response is JSON with status:'ok' (inspect the raw result in console).
  2. Re-check the threadId passed to graphFromExport — refresh the session list and retry with a valid id.
  3. Upgrade or re-sync canvas.user.js with the backend so the expected export contract matches.
  4. 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

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


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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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)