From 2c80a3a7fa2fb8678dd3b1e3d2de02012c991e3c Mon Sep 17 00:00:00 2001 From: William Date: Fri, 18 Sep 2026 14:47:07 +0800 Subject: [PATCH] fix(attachment): don't emit a pending_media skeleton on upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit attachment_upload published a `pending_media` skeleton for every uploaded image/audio/video, keyed by the new attachment id. The only thing that resolves it is attachment_send with the same id — and there is no reaper. But upload and send are separate, discretionary steps: agents routinely upload a file just to inspect it (attachment_upload → attachment_fetch) and then send a different one, or none. Every uploaded-but-unsent renderable attachment thus stranded a permanent "生成中… / generating…" bubble in the chat (the document- only guard didn't help — images/audio/video were exactly the stranding case). Make attachment_send the sole emitter. The chat consumer already handles a first-time attachment with no pre-existing placeholder, so nothing regresses on the normal upload-then-send path; we only lose a marginal skeleton during the (usually sub-second) gap between back-to-back upload and send. Co-Authored-By: Claude Opus 4.8 --- apps/agent/src/tools/attachment.ts | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/apps/agent/src/tools/attachment.ts b/apps/agent/src/tools/attachment.ts index 384f4678..65014390 100644 --- a/apps/agent/src/tools/attachment.ts +++ b/apps/agent/src/tools/attachment.ts @@ -321,25 +321,14 @@ export const createAttachmentUploadTool = ( ...(args.name !== undefined ? { name: args.name } : {}), }); - // Skeleton keyed by the new attachment id; attachment_send reuses that id as - // correlationId to swap real media in. Only renderable media gets one so an - // unsent upload, e.g. a document read, can't strand a skeleton. - const skeletonMime = result.mimeType ?? ''; - const skeletonKind = skeletonMime.startsWith('image/') - ? 'image' - : skeletonMime.startsWith('audio/') - ? 'audio' - : skeletonMime.startsWith('video/') - ? 'video' - : null; - if (skeletonKind && context.publishEvent && context.sessionId) { - context.publishEvent({ - type: 'pending_media', - sessionId: context.sessionId, - correlationId: result.id, - kind: skeletonKind, - }); - } + // Deliberately no `pending_media` skeleton on upload. Upload and send are + // separate, discretionary steps: agents routinely upload a file only to + // inspect it (attachment_upload → attachment_fetch) and then send a + // different one, or none at all. Emitting a skeleton here stranded a "生成中…" + // bubble for every uploaded-but-unsent image/audio/video, because the only + // resolver is `attachment_send` keyed on the same id (and there is no + // reaper). `attachment_send` is now the sole emitter; the chat consumer + // already handles a first-time attachment with no pre-existing placeholder. return { content: asTextContent(formatJson(result)),