History does not remember file paths that convert to [Image #1] across restarts

Open 💬 4 comments Opened Aug 14, 2026 by nullbio

What version of Codex CLI is running?

codex-cli 0.147.0

What subscription do you have?

Pro

Which model were you using?

GPT-5.6

What platform is your computer?

Linux

What terminal emulator and version are you using (if applicable)?

_No response_

Codex doctor report

What issue are you seeing?

If you restart the CLI, and go up through your history using the up arrow key, the [image #1] links are actually just the text "[image #1]" and dont represent the underlying filepath you pasted.

Furthermore, copy/paste of them also loses the actual filepath.

Also, alt+r (raw mode) does not show the actual filepath.

What steps can reproduce the bug?

Paste an image, restart codex, and get back your previous message using up arrow key.

What is the expected behavior?

It should preserve the underlying filepath for copy/paste, or for repeating messages. It should show the real filepath instead of [image #1] when in raw mode.

Additional information

_No response_

View original on GitHub ↗

4 Comments

iiwish · 14 days ago

I traced this on rust-v0.147.0 and current main (cbe85e1). The behavior comes from two related, but separate, loss points rather than image decoding itself.

1. Cross-session Up-arrow history is deliberately text-only

Same-process recall already keeps the complete draft state. The existing history_navigation_restores_remote_and_local_image_attachments test confirms that a local image path and its TextElement survive Up/Down navigation in the same TUI process.

The fresh-process path is different:

  1. Submission reduces the message to history_text before emitting AppendMessageHistoryEntry (input_submission.rs).
  2. codex-message-history::HistoryEntry stores only { session_id, ts, text } (message-history/src/lib.rs).
  3. Lookup discards the structured record and sends only entry.text back to the TUI (thread_routing.rs).
  4. HistoryEntry::new_with_at_mentions explicitly rebuilds persistent entries with empty text_elements, local_image_paths, and remote_image_urls (chat_composer_history.rs).

So [Image #1] survives as literal text, but the ownership span and path needed to turn it back into an attachment do not. This is also consistent with the scope of #9628, which added rich in-process history while explicitly leaving persistent history text-only.

Resume/replay is a useful distinction: replayed user inputs can seed a rich HistoryEntry from structured rollout items. The loss described here is the global history used by a new CLI process/thread.

2. Raw scrollback has the path, but does not render it

UserHistoryCell already carries local_image_paths, but raw_lines() renders only message and remote-image labels; it never consumes the local paths (messages.rs). That explains why Alt+R and terminal selection still expose only [Image #1] even before a restart.

Suggested bounded fix

I would avoid persisting image bytes or remote data URLs. A backward-compatible local-image payload should be enough:

  • Extend each JSONL history record with optional, defaulted local-image descriptors (path plus placeholder/range metadata), while continuing to read existing text-only rows.
  • Carry the structured history record through the lookup/batch events instead of mapping it immediately to String.
  • Rehydrate text_elements and local_image_paths when applying a persistent entry. Missing files can retain their original path and use the existing image-validation/error path on submission.
  • In raw scrollback, replace owned local-image placeholder spans with their source paths so selected text is useful and can be pasted back into the composer.

The history-owned descriptor can remain independent of codex-protocol, avoiding a new dependency from the small codex-message-history crate. The existing SaveAll/None setting and 0600 history-file permissions would continue to govern whether paths are persisted.

Regression coverage

  1. Append a legacy text-only row and a new row containing a local image; verify both deserialize and batch lookup preserves offsets.
  2. Recreate composer history metadata, fetch the rich row asynchronously, apply it, and assert the text, one image TextElement, and original PathBuf are restored.
  3. Submit the recalled entry and assert it emits UserInput::LocalImage with the original path rather than literal [Image #1] text.
  4. Assert UserHistoryCell::raw_lines() substitutes local paths while leaving ordinary literal [Image #N] text untouched.

I ran the current focused suites locally: all 12 codex-message-history tests pass, as do the TUI tests for same-session image recall and persistent text reconstruction.

If this persistence boundary and raw-mode behavior match the intended privacy/UX direction, would a maintainer be willing to invite a PR? I can implement the bounded local-image version with the regression coverage above.

jdcodes1 · 10 days ago

Confirmed on main @ 1f41cc5d92 — this is by-construction, per the code's own comment: cross-session history persists only the rendered text (with the literal [image #1] placeholder), and "Persistent history does not store attachment payloads or text-element metadata":

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/tui/src/bottom_pane/chat_composer_history.rs#L55-L70

On restart, HistoryEntry::new(text) rebuilds entries with empty text_elements/local_image_paths, so the placeholder is dead text everywhere (recall, copy, alt+r).

Notably the same constructor already round-trips mention bindings by encoding them into the persisted text and decoding on load (decode_history_mentions…). Image paths could ride the identical mechanism: encode [image #1 → /path/to/file.png] (or a structured marker) at append_entry time, decode on recall to restore the attachment reference. Cheaper interim: persist the real path in place of the bare placeholder so raw mode and copy/paste at least show it.

iiwish · 10 days ago

Thanks, this confirms my reading that the behavior is intentional at the persistent-history boundary rather than an image decoding issue. The existing mention-binding encode/decode path is a useful integration point.

My preference would be to use a backward-compatible structured marker or descriptor for local images rather than replace the placeholder with a bare path. That would let us restore both the attachment metadata and its placeholder range, while avoiding collisions with literal [Image #N] text. Raw scrollback and copy/paste could still project the source path for owned image placeholders.

If maintainers are comfortable persisting local file paths under the existing history policy, I can prepare a focused PR covering legacy/new record compatibility, append/lookup round-tripping, composer rehydration, raw scrollback rendering, missing-file behavior, and regression tests.

Would maintainers prefer reusing the existing mention marker format, or introducing a dedicated local-image descriptor?

nullbio · 9 days ago

We need to get some eyes on this from OpenAI or I feel like it'll go unnoticed for months, if not forever. It's one of those bugs that is annoying but most people just ignore because they can't be bothered to open a ticket about it (which was the case for me as well, for the last 6+ months).