BigPizzaV3/CodexPlusPlus · error · Error

会话接口返回了不匹配的数据

Error message

会话接口返回了不匹配的数据

What it means

graphFromExport validates the payload returned by the Codex++ session-read bridge before parsing it as a codex-rollout JSONL export. It throws '会话接口返回了不匹配的数据' when result.kind !== 'codex-rollout', result.session_id !== the requested threadId, or result.content is not a string. This guards the parser against the bridge returning a different payload shape (e.g. 'canvas-messages') or data for another thread.

Solutions

  1. Check result.kind returned by readCanvasSource and ensure the 'canvas-messages' branch (not graphFromExport) handles that shape
  2. Verify the bridge script and canvas.user.js are from the same version and the bridge still returns kind 'codex-rollout' with string content
  3. Re-open/refresh the canvas so threadId and the bridge's session_id are in sync
  4. Log result.session_id vs the passed threadId to find the mismatch source

Example fix

// before
const data = result.kind==='canvas-messages' ? fromNative(result) : graphFromExport(result,id,ann);
// after
if (result.kind==='codex-rollout' && result.session_id===id && typeof result.content==='string') {
  data = graphFromExport(result,id,ann);
} else if (result.kind==='canvas-messages') { data = fromNative(result); } else { status('会话数据格式不受支持'); }
Defensive patterns

Strategy: type-guard

Validate before calling

function isRolloutFor(r, threadId){ return !!r && r.status==='ok' && r.kind==='codex-rollout' && r.session_id===threadId && typeof r.content==='string'; }
if (!isRolloutFor(result, id)) { /* route to native branch or surface error */ }

Type guard

const isRolloutExport = (r, threadId) => r?.status==='ok' && r.kind==='codex-rollout' && r.session_id===threadId && typeof r.content==='string';

Try / catch

try { data = graphFromExport(result, id, ann); } catch (e) { if (e.message==='会话接口返回了不匹配的数据') { status('会话数据格式不匹配,请刷新或检查 bridge 版本'); } else throw e; }

Prevention

When it happens

Trigger: sync calls readCanvasSource and passes its result to graphFromExport when result.kind is not 'canvas-messages'; the throw fires if the bridge returned kind 'canvas-messages' or anything unexpected, session_id doesn't match the current thread id, or content is missing/non-string (e.g. bridge version mismatch or empty response).

Common situations: Running a newer/older bridge (readCanvasSource) that returns the native 'canvas-messages' shape but the userscript still routes it to graphFromExport; switching tasks between the request and the response so session_id no longer matches; a bridge failure returning a payload without content.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/ab13c0b428dbfb3e. Report an issue: GitHub.

Appendix: source

Thrown at tools/conversation-canvas/public/canvas.user.js:88

    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}));
  if(JSON.stringify(history).length>240000)throw Error('当前对话过长,暂不支持一次整理;已有画布已保留。');

View on GitHub (pinned to b1ed92e5e4)