BigPizzaV3/CodexPlusPlus · error · Error

历史消息接口返回格式无效

Error message

历史消息接口返回格式无效

What it means

Each 'thread/items/list' page must contain an array in 'data'; the script validates Array.isArray(page?.data) before iterating rows. A null page or non-array data means the response shape is unrecognized, so it throws rather than processing garbage.

Solutions

  1. Update canvas.user.js for the current Codex version (response schema likely changed).
  2. Log the raw page object to see the new response shape and adapt parsing.
  3. Check that the 'decoded message length too large' retry loop exhausted limits correctly — an overly large page may come back malformed; retry with a smaller limit.
  4. Retry history import after fully reloading the Codex app.

Example fix

// before
if(!Array.isArray(page?.data))throw Error('历史消息接口返回格式无效');
// after
if(!Array.isArray(page?.data)){console.warn('unexpected items page',page);page={data:[],nextCursor:null};}
Defensive patterns

Strategy: type-guard

Validate before calling

const page=await send('thread/items/list',...);
if(!page||!Array.isArray(page.data))throw new Error('malformed items page');

Type guard

const isItemsPage=p=>p!=null&&Array.isArray(p.data);

Try / catch

try{await importTurnItems(turn);}catch(e){if(e.message==='历史消息接口返回格式无效'){await sleep(500);return importTurnItems(turn,{smallerLimit:true});}throw e;}

Prevention

When it happens

Trigger: 'thread/items/list' returns null/undefined page, or data missing/not an array — typically after a Codex API schema change, an error swallowed upstream by send(), or a malformed RPC envelope.

Common situations: Codex client update renamed the items-list response wrapper; transient RPC failure surfaced as empty payload; hitting the internal API with wrong parameter shape after version drift.

Related errors


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

Appendix: source

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

    signal?.throwIfAborted();onProgress(`正在读取历史 ${index+1}/${turns.length}…`);
    const key=`${threadId}:${turn.id}`,cached=await cache.get(key);
    let parsed;
    if(cached&&turn.status==='completed')parsed=cached;
    else{
      const partial=turn.status==='completed'?await cache.get(`${key}:partial`):null;
      parsed=partial?.messages?.slice()||[];let itemCursor=partial?.cursor??null,limit=32;const cursors=new Set();
      do{
        signal?.throwIfAborted();
        if(cursors.has(itemCursor))throw Error('历史消息分页游标重复');
        let page;
        for(;;){
          try{page=await send('thread/items/list',{threadId,turnId:turn.id,cursor:itemCursor,limit,sortDirection:'asc'});break;}
          catch(error){
            if(limit>1&&/decoded message length too large/i.test(error.message)){limit=Math.max(1,Math.floor(limit/2));continue;}
            throw error;
          }
        }
        if(!Array.isArray(page?.data))throw Error('历史消息接口返回格式无效');
        cursors.add(itemCursor);
        for(const row of page.data){
          if(row.turnId!=null&&row.turnId!==turn.id)throw Error('历史消息所属轮次不匹配');
          const item=row.item??row;
          if(!['userMessage','agentMessage'].includes(item.type))continue;
          if(typeof item.id!=='string')throw Error('历史消息缺少稳定 ID');
          const role=item.type==='userMessage'?'user':'assistant';
          const content=role==='user'?(item.content??[]):[{text:item.text??''}];
          const message=parseMessage({type:'response_item',payload:{type:'message',id:item.id,role,phase:role==='assistant'?(item.phase??'final_answer'):'',content}},item.id);
          if(message)parsed.push({...message,turnId:turn.id,turnStatus:turn.status});
        }
        itemCursor=page.nextCursor??null;
        onProgress(`正在读取历史 ${index+1}/${turns.length} · 已读 ${parsed.length} 条消息…`);
        if(turn.status==='completed'&&itemCursor!==null)await cache.set(`${key}:partial`,{messages:parsed,cursor:itemCursor});
      }while(itemCursor!==null);
      if(turn.status==='completed'){await cache.set(key,parsed);await cache.delete(`${key}:partial`);}
    }
    // Older completed-turn caches contain text and IDs but no turn metadata.

View on GitHub (pinned to b1ed92e5e4)