Skip to content

Codex chat: short replies leave a live session blank until a longer answer is rendered #283

Closed

Description

@devswha

What happened

A live Codex pane stayed in the sidebar, but its chat lens became empty after a request to combine three screenshots into one image. The original rollout still existed and contained the request and answer. This was a transcript-resolution failure, not a closed pane or lost session data.

During diagnosis, GET /api/pane/conversation?pane_id=<pane-id> returned HTTP 200 with:

{"source":"scrollback","turns":[]}

After a longer diagnostic commentary appeared in the same pane, the unchanged installed server resolved the same rollout and returned source: "codex-transcript" with six turns, including the original image request and answer. No server restart, configuration change, or source patch was needed for this temporary recovery.

Environment and frequency

  • Observed on 2026-10-02 KST.
  • Installed herdr web ui 0.3.40, revision ef900557aa725845dfea1013b468538baf20b2d4, herdr plugin / managed installation.
  • Local server and pane: Ubuntu 24.04.4 LTS, Linux x86_64; herdr 0.9.0, protocol 22.
  • Codex session metadata: 0.159.3, originator: "codex-tui", source: "vscode", thread_source: "user".
  • Client browser/device was not captured. The empty response and recovery were independently verified through read-only localhost API calls.
  • Once observed live; the matcher failure is deterministic with the attached message excerpt. The last working version is unknown.

Confirmed cause

answerAnchor and matchCodexTranscript require at least 64 normalized letters/digits in a single assistant-message anchor, with at least 12 distinct characters. Markdown link targets are removed because Codex renders links differently.

All three assistant texts in the image task fail the length condition:

Session message Normalized anchor lengths
First commentary 40
Second commentary 29
Final answer with an image download link 38, then an empty trailing fragment

The subsequent diagnostic commentary has 65 normalized letters/digits and matches successfully. All initial fragments have more than 12 distinct characters, so length is the blocking condition.

For this pane, agent.get reported agent: "codex" without agent_session; foreground argv was the Codex launcher and binary, with no resume <thread-id>. The pane therefore depended on the visible assistant-message fallback rather than an explicitly reported session path or resumed thread. The resolver and HTTP fallback are in codexTranscriptPath, resolveTranscript, and the conversation route.

The rollout contained large image records, but the minimal reproduction below needs no image payloads. The evidence establishes the short-message matching failure; it does not establish image-file corruption or deletion.

Reproduction

Live scenario:

  1. Open a fresh/shared-app-server Codex conversation for which herdr does not report an explicit session path/id and no rendered answer has bound the pane to a rollout yet.
  2. Ask it to combine three images. The observed short commentary and final-answer messages are in the session log below.
  3. Open the chat lens: the pane remains in the roster, while the conversation API returns the empty scrollback fallback.
  4. Let the same session emit the 65-character diagnostic commentary below: the resolver matches it, and native conversation history returns.

Pure matcher reproduction, without herdr or access to the original session:

Save the JSONL excerpt below as session-log.redacted.jsonl in the repository root. Save this script there as reproduce.ts, then run bun reproduce.ts:

import { matchCodexTranscript, parseCodexTranscript } from "./server/codex.ts";
const text = await Bun.file(new URL("session-log.redacted.jsonl", import.meta.url)).text();
const turns = parseCodexTranscript(text);
const assistant = turns.filter((turn) => turn.role === "assistant");
const prose = (turn: (typeof assistant)[number]) => turn.parts.filter((part) => part.kind === "text").map((part) => part.text);
const initial = prose(assistant[0]!);
const recovery = "확인됐어요. 세션은 살아 있지만, 서버가 이 세션의 Codex 대화 파일을 찾지 못해 빈 채팅을 반환하고 있습니다. 파일 연결이 어디서 끊겼는지 확인하고 있어요.";
const candidate = [{ path: "session", text }];
const before = matchCodexTranscript(initial.join("\n"), candidate);
const after = matchCodexTranscript(recovery, candidate);
console.log(JSON.stringify({ before, after, anchors: initial.map((text) => text.split(/\]\([^)\s]*\)/).map((part) => part.normalize("NFKC").replace(/[^\p{L}\p{N}]/gu, "").slice(-160).length)), recoveryLength: recovery.normalize("NFKC").replace(/[^\p{L}\p{N}]/gu, "").length }, null, 2));
if (before !== null || after !== "session") throw new Error("Evidence no longer reproduces the observed boundary");

Verified output on the unchanged code:

{
  "before": null,
  "after": "session",
  "anchors": [[40], [29], [38, 0]],
  "recoveryLength": 65
}

Session log and API evidence

This is an excerpt from the actual Codex rollout, preserving message text and timestamps. Local paths and thread identity are redacted. System/developer instructions, account identifiers, reasoning, tool inputs/outputs, question-reply envelopes, and inline image/base64 payloads are omitted. The full original rollout remains local; this is not a full transcript upload.

Sanitized session-log.redacted.jsonl (8 records)
{"timestamp":"2026-10-01T19:52:23.207Z","type":"session_meta","payload":{"id":"<thread-id>","cwd":"$REPO","originator":"codex-tui","cli_version":"0.159.3","source":"vscode","thread_source":"user"}}
{"timestamp":"2026-10-01T19:52:24.718Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"@$REPO/.herdr-web-ui/paste-20261001-195208-540dc6f5.jpg @$REPO/.herdr-web-ui/paste-20261001-195209-716cff0e.jpg @$REPO/.herdr-web-ui/paste-20261001-195209-175abbab.jpg \n\n이거 세개 사진 합처서 하나의 이미지로 만들어줘."}]}}
{"timestamp":"2026-10-01T19:52:26.552Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"세 사진을 확인하고 한 장으로 합칠게요. 이미지 편집을 위해 imagegen 스킬을 사용할게요."}],"phase":"commentary"}}
{"timestamp":"2026-10-01T19:52:39.950Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"세 화면이 모두 보이도록, 올려주신 순서대로 가로로 나란히 배치할게요."}],"phase":"commentary"}}
{"timestamp":"2026-10-01T19:53:50.449Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"imagegen으로 세 사진을 올려주신 순서대로 가로로 합쳤어요.\n\n[합친 이미지 다운로드]($REPO/.herdr-web-ui/combined-20261002.png)"}],"phase":"final_answer"}}
{"timestamp":"2026-10-01T19:54:55.386Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"뭐야 이 세션 왜 갑자기 채팅에서 안보이지? 허들웹"}]}}
{"timestamp":"2026-10-01T19:54:57.876Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"herdr 웹에서 이 세션이 목록에서 사라진 원인을 확인할게요. 실행 중인 서버 상태와 세션 목록을 불러오는 흐름부터 살펴보겠습니다."}],"phase":"commentary"}}
{"timestamp":"2026-10-01T19:55:29.946Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"확인됐어요. 세션은 살아 있지만, 서버가 이 세션의 Codex 대화 파일을 찾지 못해 빈 채팅을 반환하고 있습니다. 파일 연결이 어디서 끊겼는지 확인하고 있어요."}],"phase":"commentary"}}
API/RPC observations before and after recovery
{
  "before": {
    "health": {
      "ok": true,
      "herdr": {
        "version": "0.9.0",
        "protocol": 22
      },
      "web_ui": {
        "revision": "ef900557aa725845dfea1013b468538baf20b2d4"
      }
    },
    "pane": {
      "present_in_session_snapshot": true,
      "agent": "codex",
      "agent_session_reported": false,
      "foreground_processes": [
        "node <codex-launcher>",
        "<codex-binary>"
      ],
      "resume_thread_in_argv": false
    },
    "conversation": {
      "status": 200,
      "source": "scrollback",
      "turns": []
    }
  },
  "after_65_character_commentary": {
    "conversation": {
      "status": 200,
      "source": "codex-transcript",
      "turn_count": 6,
      "original_image_request_and_answer_present": true
    }
  },
  "provenance": "Sanitized observations from read-only localhost API/RPC calls during this diagnosis; not fabricated responses and not a saved network trace."
}

Expected behavior and regression coverage

A distinctive short-answer conversation should be resolvable using sufficient session-specific evidence, without requiring the agent to produce a longer answer merely to make its chat visible. Recovery after a longer answer is a workaround; no fix has been applied.

A fix must preserve the existing safeguards: do not choose a conversation by cwd/mtime recency alone, do not bind arbitrary generic text such as Done, and do not display another pane's conversation. Candidate approaches include matching multiple distinctive rendered assistant fragments together or using a reliable pane-specific native identity.

Cover the short Korean commentary/final-answer sequence, terminal wrapping and rendered file links, ambiguous short text shared by several rollouts/panes, and /new or /resume transitions. Keep the existing Done, user-context, tool-output, and previous-session rejection tests in server/codex.test.ts. Any end-to-end resolver/API regression test should use the isolated test herdr and temporary state rather than the live pane.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions