Skip to content

[Bug]: Relative Markdown image in a remote thread shows the desktop's local file when the remote file is missing #17392

Description

@jieyuexing

Note

Drafted by Claude Opus 5.5 and GPT-6 Astra in T3 Code; reviewed and posted by @jieyuexing.

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/web (logic in packages/client-runtime)

Impact

Minor bug or occasional failure

Steps to reproduce

  1. Desktop app (macOS) with its primary local environment, connected to a second, remote environment (Linux server over HTTPS).
  2. On the remote environment, register a project rooted at /tmp/t3-repro/repo. Make sure /tmp/t3-repro/repo/docs/report.png does not exist there.
  3. On the desktop machine, create a distinguishable PNG (solid red) at the same absolute path, /tmp/t3-repro/repo/docs/report.png.
  4. In the remote project, start a thread whose message contains the relative image ![report](docs/report.png) (I used the user prompt and had the agent echo the same line).
  5. Open that thread in the desktop app.
  6. Control: put a different PNG (solid green) at /tmp/t3-repro/repo/docs/report.png on the remote environment and view the thread again.

Expected behavior

A relative image authored in a remote thread stays scoped to that thread's environment. When the remote file is missing, the image shows as unavailable.

Actual behavior

With the file missing on the remote, the desktop renders the red local file in both the user message and the assistant reply. After the green file is placed on the remote, the same thread shows the green remote file, so the first request does target the remote environment correctly; the wrong file comes from the desktop-local fallback.

Call chain on current main:

  1. classifyMarkdownImageSource resolves docs/report.png against the thread cwd and returns { _tag: "WorkspaceFile", path: "/tmp/t3-repro/repo/docs/report.png" }. Whether the author wrote a relative or absolute path is not kept.
  2. ChatMarkdown puts that absolute path into a media-file resource for the remote threadRef.environmentId (classification, resource).
  3. The remote returns AssetWorkspaceAssetNotFoundError (confirmed by calling assets.createUrl on the remote server directly). createAssetEnvironmentAtoms sees an absolute media path and retries against the desktop's local environment, which serves its own file at that path.

The local-media fallback from #10619 is described as "relative paths and authorization failures do not fall back", and assets.test.ts covers a relative resource.path. That test passes the relative path directly, so it does not cover the Markdown path that is resolved to absolute before reaching the router. If the description only meant the RPC layer, maintainer guidance on the intended Markdown semantics would help.

The overlap needs the same absolute path on both machines, so it is uncommon, but when it happens the client silently shows a different file from the one the remote thread refers to, with no indication of where it came from.

Version or commit

Desktop 0.0.46-nightly.20261008.2833 (a personal build of that nightly with translation-only changes). Remote server 0.0.46-nightly.20261008.2833. Against main c9fa1367caff4cd6595386814e1f8bb950d3d3ef, the build's markdownImages.ts, state/assets.ts, and asset contract are identical; ChatMarkdown.tsx differs only by newer heading-anchor changes on main, and AssetAccess.ts by one import path.

Environment

Desktop: macOS 26.7, Apple Silicon. Remote: Linux x64 T3 server, connected over Tailscale HTTPS. Provider: Codex (not relevant to the rendering path).

Supporting fixture

Besides the desktop run, a small script that combines the real classifyMarkdownImageSource and createAssetEnvironmentAtoms with in-memory remote/local endpoints shows the calls [remote, local] for the relative Markdown case, against [remote] for a raw relative resource path, an authorization failure, a PDF, or no local environment. An explicit absolute PNG still gets [remote, local], which is the intended #10619 behavior. I can attach the script if useful.

Suggested scope

Keep the authored path scope (relative vs. absolute) through image classification and resource construction, and only allow the desktop-local fallback for paths that were absolute as written. Include that scope in the query identity so a relative and an absolute reference to the same resolved path cannot share a cached result. This does not touch remote-first routing or the existing absolute-path fallback.

This is separate from the subdirectory link base problem tracked in #8251 / #10553.

Workaround

Write image paths as absolute paths that exist on the thread's environment, or avoid having the same absolute path on the desktop machine.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions