Skip to content

Editing queued messages retains removed path attachment references #55179

Open

Description

@dakjdakd

Description

Editing a queued message that contains ordinary prompt text and a path attachment can leave the removed attachment in the text sent to the model. Keeping the attachment and changing only the prompt text has a related effect: the model receives the same attachment reference twice. In both cases, the visible composer and the replacement prompt's attachment metadata can disagree with the replacement prompt's actual text.

For example, queue the text summarize data with an uploaded file stored at /repo/confidential.csv. The app represents the upload as a path attachment and builds the model-visible prompt as:

summarize data
Attached file: `/repo/confidential.csv`

The presentation metadata separately stores displayText: "summarize data" and the attachment reference. When editing the queued message, the composer displays the ordinary text and restores the attachment as a removable card. Change the text to Use only public data, remove the file card, and save the edit. The replacement metadata correctly has an empty attachments list, but the replacement text still contains Attached file: /repo/confidential.csv, with the same backtick formatting shown above. The instruction to remove the attachment therefore does not remove its reference from the prompt the model receives.

If the file card is kept instead, saving a text change produces two copies of the attachment reference: one generated from the retained card and another copied from the original message. This can be reproduced without any model response; it is already present in the replacement request to the session prompt endpoint.

The cause is the treatment of the original prompt's text suffix in packages/app/src/session/composer/queue.ts. editedPromptInput rebuilds the request from the current composer contents, then retains everything after the original display text as notes. That suffix contains both comment notes and path attachment references. Preserving the entire suffix makes sense for retaining review context, but it also copies attachment references that the new request has already regenerated or that the user has removed.

The expected behavior is for the current composer attachments to determine the replacement's attachment references. Removing a card should remove its generated reference from both the model-visible text and presentation metadata. Retaining a card while editing the text should produce one reference. Existing review and browser comments should survive once, along with their file context, because those comments belong to the queued message being edited. A literal Attached file: phrase typed by the user must remain ordinary text.

The fix can use the existing structured prompt presentation to distinguish comments from attachments. When a hidden suffix belongs to a readable app presentation, rebuild the retained notes from the presentation's comments and let the newly built request supply the current path references. For messages from another client without that structured presentation, preserve the opaque suffix rather than trying to remove attachment-looking text with a regular expression. This keeps the change grounded in the metadata that owns the generated content.

The accompanying local patch follows that approach. The existing session queue regression suite now covers removing and retaining a path attachment, checking the replacement request rather than relying on a model response. The five focused cases pass while preserving review comments, browser comments, a user-typed attachment-looking phrase, and the comment's file:// context. They also cover opaque context from another client, deleted file mentions, and comment-only editing. The resulting request and attachment metadata agree with the edited composer.

This report concerns messages with non-empty display text and a hidden generated suffix.

Plugins

No plugin is required to reproduce this queue editing behavior.

OpenCode version

App package 2.0.28, reproduced from the dev source at 1015cb67d. The affected code is unchanged through 23e3493ab.

Steps to reproduce

  1. Open an app session with execution in progress, so follow-up messages can be queued.
  2. Write a non-empty prompt, such as summarize data, attach a non-image file that the app stores as a path reference, and queue the message.
  3. Click the queued message to edit it. Confirm that the prompt text appears in the editor and the file appears as an attachment card.
  4. Change the text to Use only public data, remove the file card, and save the queued edit.
  5. Inspect the replacement session prompt request. Its metadata has attachments: [], but its text still includes the removed file's generated reference.
  6. Repeat with another queued message, retaining the attachment card while changing the prompt text. The replacement request contains the same generated file reference twice.

Screenshot and/or share link

The discrepancy is in the submitted request text. The reproduction above shows the relevant prompt content; no shared session is needed.

Operating System

Windows. The cause is in shared app prompt construction rather than an operating-system-specific upload implementation.

Terminal

PowerShell for local verification; the affected editing flow is in the app composer.

Linked fix

A focused fix is available in #55180.

Activity

  1. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I can confirm this on the latest dev (23e3493ab). The relevant composer code is unchanged since 2.0.28.

    The script below calls the app's real editedPromptInput with a queued message (summarize data plus a path attachment to /repo/confidential.csv) and changes the text to Use only public data:

    • Card removed: the replacement text still ends with Attached file: `/repo/confidential.csv` while metadata.attachments is [].
    • Card kept: the replacement text contains that reference twice.

    Related: #55180 (your proposed fix) and #49081 (the earlier change that restored queued attachments to the composer when editing).

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces anomalyco/opencode#55179 against the app's real editedPromptInput.
    # Usage: REPO=/path/to/opencode-checkout bash repro.sh
    set -euo pipefail
    REPO=${REPO:-/workspace/repo}
    DIR="$REPO/packages/app/src/session/composer"
    # editedPromptInput is module-private; make a temporary copy that exports it.
    sed 's/^async function editedPromptInput(/export async function editedPromptInput(/' "$DIR/queue.ts" > "$DIR/queue-repro-export.ts"
    cat > "$DIR/queue-repro.test.ts" <<'EOF'
    import { expect, test } from "bun:test"
    import { editedPromptInput, queuedPromptAttachments, type QueuedPrompt } from "./queue-repro-export"
    
    const ref = "Attached file: `/repo/confidential.csv`"
    const item = {
      id: "msg_q", sessionID: "ses_1", time: { created: 1 }, type: "user", delivery: "queue",
      payload: {
        text: "summarize data\n" + ref,
        metadata: {
          displayText: "summarize data",
          comments: [],
          attachments: [{ name: "confidential.csv", mime: "text/csv", path: "/repo/confidential.csv" }],
        },
      },
    } as unknown as QueuedPrompt
    
    const text = "Use only public data"
    const textPart = { type: "text", content: text, start: 0, end: text.length } as any
    
    test("removing the path attachment card drops its reference", async () => {
      const out = await editedPromptInput("ses_1", "/repo", item, [textPart], text)
      console.log("removed card ->", JSON.stringify({ text: out.text, attachments: out.metadata.attachments }))
      expect(out.metadata.attachments).toEqual([])
      expect(out.text).not.toContain(ref)
    })
    
    test("keeping the card produces exactly one reference", async () => {
      const card = queuedPromptAttachments(item)
      const out = await editedPromptInput("ses_1", "/repo", item, [textPart, ...card] as any, text)
      console.log("kept card ->", JSON.stringify({ text: out.text, attachments: out.metadata.attachments }))
      expect(out.text.split(ref).length - 1).toBe(1)
    })
    EOF
    cd "$REPO/packages/app"
    bun test --conditions=solid --preload ./happydom.ts src/session/composer/queue-repro.test.ts || status=$?
    rm -f "$DIR/queue-repro-export.ts" "$DIR/queue-repro.test.ts"
    exit ${status:-0}

    Run it: bash repro.sh

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions