Skip to content

feat(chat-runtime,host-service): every adapter hands the agent its attachments - #8849

Closed
superset-app[bot] wants to merge 3 commits into
harshith/attachments-host-storefrom
harshith/attachments-adapters
Closed

superset-app[bot] wants to merge 3 commits into
harshith/attachments-host-storefrom
harshith/attachments-adapters

Conversation

@superset-app

@superset-app superset-app Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #8846 (plan PR 3). It needs that PR's handOutAttachment / handOutAttachmentByFileId and the fileId index. The base is #8846's branch so the diff shows only this change. Retarget to main once #8846 merges.

#8843 (AdapterEvent kind → type) touches the same adapter files. A dry-run merge of this branch with #8843's head is conflict-free (git merge-tree), and the new code here emits no events. If #8843 lands first, I'll rebase this on main.

Summary

PR 4 of the attachments plan (v6), seam 4: every harness gets the file, from whichever id the part carries. Claude SDK and Codex stop dropping attachments, and all three adapters share one host resolver.

Why

  • The Claude SDK adapter turned an attachment part into the text [name]. The Codex adapter dropped it, with the notice "Attachments are not supported by the codex harness". Only ACP sent the file. That is why desktop chat still puts attachments into the text as path tags, and so desktop and mobile can't render each other's attachments.
  • A cloud part (fileId) has to resolve on a box whose store is empty, for example a recreated box resuming a session. No adapter could do that.

How it works

  • One resolver (host-service/src/chat-v3/resolveAttachment.ts, passed to ACP, Claude and Codex from mount.ts):
    • fileId set: use the box's cached copy (handOutAttachmentByFileId), or else importCloudAttachments it now and use that.
    • otherwise attachmentId: the host store entry (handOutAttachment).
    • Both paths stamp lastUsedAt. Bytes are read (base64) only for images. Other files go by path, so a 200 MB PDF is not read into memory. A thrown error (for example a failed import) goes to Sentry (reportAttachmentResolveFailure), and the adapter gets null.
  • chat-runtime types: AttachmentPart, ResolvedAttachment, ResolveAttachment in harness/types.ts. ACP's resolveAttachment now gets the whole part instead of an attachmentId. AcpAttachment stays as an alias.
  • attachedFilesBlock (harness/attachedFiles/): # Attached files … - <name>: <path>. Store files are named <id>/<id>.<ext>, so a bare path would give the agent only a UUID. agents.run's terminal-launch block uses it too, so it now lists names as well.
  • Claude SDK adapter: an image (png/jpeg/gif/webp) goes as a base64 image block in the SDK's content-array form. Any other file goes as the attached-files text. A file that can't be resolved goes as [name: could not be read].
  • Codex adapter: an image goes as app-server's { type: "localImage", path }. Any other file goes as the attached-files text, and an unresolved one as the same "could not be read" text. As the plan says, we send the image even to a text-only model and let app-server reject it.
  • Order: both adapters chain attachment resolution, so a prompt sent while an earlier one is still resolving goes second. A text-only prompt with nothing queued is still pushed synchronously, the same as before.
  • Protocol: the attachment part now needs attachmentId or fileId (it required attachmentId before), so a client can send a cloud part by fileId alone. Mobile's UserMessageBubble keeps showing parts that have a host id and skips a fileId-only part for now. The mobile PR adds the cloud branch.
  • ATTACHMENT_PARTS_MIN_HOST_VERSION = "1.41.0" in @superset/shared/attachment-limits. desktop-v1.40.0 is the latest tag, and this is not in it. If this misses 1.41.0, raise the constant before the desktop PR uses it. hostReadsAttachmentParts(version) in @superset/shared/host-version is the client-side check, so desktop (feat(desktop): attachments go where the workspace lives, as chat parts #8851) and mobile use the same rule. It coerces the version, so a canary or dev build of 1.41.0 or later counts (tested at, above and below the floor, and for an unknown version).

Where this differs from the plan, or the plan is out of date against main

  • The resolver is in its own file, chat-v3/resolveAttachment.ts, not inside acpHarnesses.ts, because three adapters now use it. It is passed through createChatV3Mount({ api }) so the box import has an API client.
  • No PostHog event. The plan asks for Sentry and PostHog on every resolve failure. host-service has no PostHog client, so this PR sends Sentry only.
  • I did not do the "store path outside the worktree" check for Codex and Claude. The plan wants it from PR 3's fixtures and this PR's manual run. Both adapters pass the absolute store path, and the plan's argument (app-server and the SDK read as the host user, not inside the agent's sandbox) is not verified here. See Testing.

tRPC / version compatibility

  • No tRPC procedure changes.
  • The schema change to the part is looser: attachmentId is now optional, and at least one of the two ids is required. An older host still requires attachmentId, so a fileId-only part sent to it fails validation. That is why clients send parts only at or above ATTACHMENT_PARTS_MIN_HOST_VERSION. No client on main sends a fileId-only part yet. Parts the host builds itself (feat(host-service): sweep the attachment store by last use, and keep fileId on cloud copies #8846) carry both ids.

Testing

  • chat-runtime bun test: 384 pass, 0 fail (the Claude adapter suite is 38/38 after 45ec975). New tests:
    • Claude: an image part becomes an image block; a non-image becomes name + path; an unreadable part is named; a prompt sent while an attachment resolves goes second.
    • Codex (real recorded text fixture, turn/start params now recorded by FixturePlayer): an image becomes localImage; a non-image becomes name + path; an unreadable one is named.
    • ACP: the resolver gets the whole part (fileId-only). The existing image / non-image / missing tests were kept and moved to the new resolver type.
    • attachedFilesBlock format.
    • Claude: an empty text part is dropped (commit 45ec975). An attachment-only message can carry one, and the Anthropic API rejects an empty text block.
  • host-service resolveAttachment.test.ts: a host part reads from the store; a fileId part on a box with a cached copy does not import; a fileId part on an empty store imports; fileId wins when both are set; a missing id gives null; a failed import is reported and gives null; a non-image's bytes are not read.
  • host-service bun test: 2651 pass, 1 fail. The failure is the same health.check sandbox-mode test that fails on feat(host-service): sweep the attachment store by last use, and keep fileId on cloud copies #8846: it reads this machine's real boot stamps, because this workspace is a real cloud box.
  • bun run typecheck passes in chat, chat-runtime, host-service, cli and apps/desktop. apps/mobile has the same 190 errors with and without this PR (I diffed the error lists; this PR first added 2 in UserMessageBubble, which are fixed). Biome is clean on the changed files.
  • Not verified: no live Claude or Codex run. I did not check that either agent actually opens an image or a store path outside the worktree, and I did not try a recreated box. Those are the plan's manual checks for this PR and the desktop PR.

Known limitations

  • On a host older than 1.41.0, a fileId-only part fails validation. That is what the version gate is for (desktop PR).
  • On a box, a cloud part whose copy was never imported and whose file is gone from storage reaches the agent as "could not be read". That is the intended fallback.

…tachments

Part of the attachments plan (PR 4, seam 4).

The Claude SDK adapter rendered an attachment part as `[name]`, and the Codex
adapter dropped it with a notice. Only ACP resolved it. This is why desktop
chat still flattens attachments into path tags in the text.

Now all three take one resolver from the host (chat-v3/resolveAttachment.ts).
It branches on the identity the part carries. `fileId` is a cloud workspace's
storage object: served from the box's cached copy, or imported now when the
store is empty, as on a recreated box. `attachmentId` is the host store entry
itself. Each hand-out stamps lastUsedAt for the sweep. Bytes are read only for
images, since every adapter gives other files by path.

- Claude: an image goes as a base64 image block in the SDK's content-array
  form. Any other file goes as a "# Attached files" line with its name and
  path, because store files are named by id.
- Codex: an image goes as app-server's `localImage` by path. Any other file
  goes as the same text block.
- An unreadable file is named to the agent, not dropped silently.
- Resolution is chained per adapter, so prompt order holds. A text-only prompt
  with nothing queued is still pushed synchronously.

The protocol's attachment part now needs `attachmentId` or `fileId` (it
needed `attachmentId` before), so a client can send a cloud part by fileId
alone. Clients send parts only to hosts at or above
ATTACHMENT_PARTS_MIN_HOST_VERSION (1.41.0, the first release with this
change). agents.run's attached files block uses the same attachedFilesBlock,
so terminal launches also list files by name.

A failed resolve reports to Sentry. The host has no PostHog client, so the
plan's PostHog event is not sent from here.
@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e53fa528-aec5-4a59-aacf-28467d39f387

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

@greptile-apps

greptile-apps Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] Attachment delivery changes are contained to agent prompt workflows.

The PR appears safe to merge, with a non-blocking improvement to avoid unused image reads for Codex.

Findings

  1. P2 Codex reads unused image bytes ▶
Fix with agent prompt
### Issue 1
packages/host-service/src/chat-v3/resolveAttachment.ts:41-43
`createAttachmentResolver` reads every image and builds a base64 string, even for Codex, which only uses `file.path`. Large images therefore add an unnecessary file read and memory use before Codex can start. Let adapters request bytes only when they need them, so Codex can resolve images by path.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds one host resolver for attachments identified by attachmentId or fileId, then wires it into ACP, Claude, and Codex.

  • Claude receives attached images and file paths in its prompts.
  • Codex receives attached images and file paths in its prompts.
  • One host resolver finds attachments for ACP, Claude, and Codex.
  • Attachment parts can identify a file by either host ID or cloud ID.
  • Agent prompts pair each attached file's name with its host path.

Acknowledged by superset-app[bot]: mobile intentionally skips fileId-only attachments for now; older hosts need the planned client version gate; PostHog reporting is omitted because host-service has no client; live agent access to files outside the worktree remains unverified; missing cloud files use the intended unreadable-file fallback.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Attachment part] --> B{Has fileId?}
  B -->|Yes| C[Look up cached cloud copy]
  C -->|Missing| D[Import cloud file]
  C -->|Found| F[Return stored file]
  D --> F
  B -->|No| E[Look up attachmentId]
  E --> F
  F --> G{Adapter}
  G --> H[Claude: image bytes or named path]
  G --> I[Codex: local image path or named path]
  G --> J[ACP: image bytes or file link]
Loading

Reviews (1) · Last reviewed commit: "feat(chat-runtime,host-service): every a..." · Reviewed by Greptile

Comment on lines +41 to +43
data: mimeType.startsWith("image/")
? (await readFile(found.path)).toString("base64")
: "",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Codex reads unused image bytes

createAttachmentResolver reads every image and builds a base64 string, even for Codex, which only uses file.path. Large images therefore add an unnecessary file read and memory use before Codex can start. Let adapters request bytes only when they need them, so Codex can resolve images by path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/host-service/src/chat-v3/resolveAttachment.ts
Line: 41-43

Comment:
**Codex reads unused image bytes**

`createAttachmentResolver` reads every image and builds a base64 string, even for Codex, which only uses `file.path`. Large images therefore add an unnecessary file read and memory use before Codex can start. Let adapters request bytes only when they need them, so Codex can resolve images by path.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

… attachments

An attachment-only message can carry an empty text part, and the Anthropic
API rejects an empty text content block.
…floor

Desktop and mobile both decide between attachment parts and path tags from
a host's version. One function next to ATTACHMENT_PARTS_MIN_HOST_VERSION keeps
the two clients on the same rule. It coerces the version, so a canary or a
dev build of a release at or above the floor counts.

Copy link
Copy Markdown
Collaborator

Folded into #9064, which now carries the whole attachments plan (commit list in its description). Closing in favor of that PR.

This branch was previously deployed

1 inactive deployment
preview — 69356759 Deployed Oct 24, 2026 by superset-app[bot] via Deploy Database (Neon) #12697
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants