Skip to content

feat(host-service): sweep the attachment store by last use, and keep fileId on cloud copies - #8846

Closed
superset-app[bot] wants to merge 1 commit into
mainfrom
harshith/attachments-host-store
Closed

superset-app[bot] wants to merge 1 commit into
mainfrom
harshith/attachments-host-store

Conversation

@superset-app

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

Copy link
Copy Markdown
Contributor

Summary

PR 3 of the attachments plan (v6): host-service + shared. It covers seam 8 (the limits module), seam 2 (retention of the host store by last use), and the fileId carry-through of seam 3. It does not depend on #8704 (which now includes the API change from #8712). This PR uses only attachment.resolve, which is on main.

Why

  • The host store never forgets. attachments.upload / importFromCloud write into $HOST_MANIFEST_DIR/attachments/<id>/, and nothing removes those files. There is no session delete to hook: chat-v3 only has closeSession, nothing deletes chat.db rows, and a terminal launch has no session.
  • The first prompt of a cloud session loses its identity. A cloud launch imports its attachments from storage at first boot, and the host builds the first-prompt parts from that store record, so the part has only a host attachmentId. That is the one kind of attachment that a reader without the box (web, a shared view, a resumed session on a recreated box) cannot render.
  • Limits are spread out. MAX_INLINE_ATTACHMENT_BYTES was a host-local constant, and the per-send 10 was hard-coded.

How it works

  • lastUsedAt in metadata.json. Upload and import set it. Every path that gives a file to a client or an agent goes through the new handOutAttachment, which sets it again: attachments.read, the # Attached files block of agents.run, and the ACP resolver in chat-v3/acpHarnesses.ts.
  • sweepAttachmentStore (attachments/sweep.ts) runs once at startup and then daily, beside the project purge. It does not run in a sandbox: a box's store is only a cache that dies with the box. It removes entries whose lastUsedAt is older than ATTACHMENT_RETENTION_DAYS (30). An entry written before this change has no stamp. The sweep stamps it, so it gets the full 30 days. For each entry, the metadata check and a renameSync to .sweeping-<id> run in the same tick, so a hand-out on this event loop cannot slip between them. The recursive delete then runs async (fs/promises.rm), off the main loop. A leftover .sweeping-* from a crash is removed on the next pass.
  • Lookup by fileId (for seam 4). importCloudAttachments writes fileId into the metadata and into a one-line index file .by-file-id/<fileId> → attachmentId. handOutAttachmentByFileId reads it in O(1), checks it against the metadata, and stamps the entry. Delete and the sweep remove the index entry with the store entry.
  • First-prompt parts carry fileId. chatLaunchTarget spreads fileId (and sizeBytes) from the store metadata into each part. The sandbox self-seed already sends the imported ids through it, so a cloud launch's first prompt now names its storage object. importFromCloud's result also gains fileId (an additive output field).
  • Protocol. userContentSchema's attachment part gains optional fileId (uuid) and sizeBytes. These are additive. attachmentId is still required. Making it optional is the next PR, together with the resolver that can serve a part without it.
  • Limits. @superset/shared/attachment-limits gains MAX_INLINE_ATTACHMENT_BYTES (moved from the host's constants.ts, which is deleted; the attachments index still re-exports it), MAX_THUMBNAIL_BYTES, and ATTACHMENT_RETENTION_DAYS. importFromCloud, materializeIntoWorkspace and cloudAgentLaunch.attachmentFileIds use MAX_ATTACHMENTS (still 10, so their inputs are unchanged).

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

  • A first-prompt part carries both attachmentId and fileId. The plan says "exactly one of the two is set". On main today, every adapter and every released mobile build reads attachmentId, so a fileId-only part would reach the agent as "could not be read" until the resolver from the next PR ships. Readers still branch on fileId first. The next PR can drop attachmentId from host-built parts once its resolver is in place.
  • ATTACHMENT_PARTS_MIN_HOST_VERSION is not here. It is "the first host release carrying seam 4", so it belongs in the next PR.
  • MAX_THUMBNAIL_BYTES is defined but not used yet. Desktop's useChatImageSource keeps its local copy until the desktop PR switches it over. That keeps this PR to host-service + shared.
  • Not changed: the two .max(10) in packages/trpc (cloudWorkspace.create's attachmentFileIds, attachFiles). They are API code, outside this PR.
  • Line references are out of date. agents.ts:737 is now sessions/create-chat-session.ts (createChatSession), which spreads chatLaunchTarget's attachments into the prompt.

tRPC compatibility

  • attachments.importFromCloud: the output gains fileId, which is additive. The input is unchanged (max(MAX_ATTACHMENTS) = max(10)).
  • attachments.materializeIntoWorkspace: the input is unchanged, for the same reason.
  • attachments.read, upload, delete: the contracts are unchanged. read now also writes lastUsedAt.
  • The chat protocol's attachment part: two new optional fields. Older hosts accept them, because the part is a looseObject.
  • No new host procedure, so no client guard and no MIN_HOST_SERVICE_VERSION change.

Testing

  • sweep.test.ts: the sweep removes an entry unused past retention and keeps a recent one; an unstamped entry is stamped, not removed; an entry handed out after the pass started is kept; the fileId index is removed with its entry; handOutAttachment / handOutAttachmentByFileId stamp what they hand out.
  • attachments.test.ts: read stamps the entry; importCloudAttachments records the fileId, and a lookup by that fileId finds the copy (with a local Bun.serve standing in for the presigned GET).
  • agents.test.ts: a cloud launch's first-prompt part carries the fileId it was imported from (import, then chatLaunchTarget); the agents.run attached files block (buildTerminalAgentLaunch) stamps what it lists.
  • bun run typecheck passes in packages/shared, packages/chat, packages/chat-runtime, packages/host-service and apps/desktop. Biome is clean on the changed files.
  • packages/host-service bun test: 2644 pass, 1 fail. The failure is health.check > serialises the current boot's stamps … in sandbox mode. It reads the real boot stamps of the machine it runs on, and this workspace is a real cloud box, so ready is true and bundle is set. It does not touch attachment code. no-main-loop-blocking.test.ts passes, and the rmSync count in storage.ts is still 2.
  • packages/shared (1444) and packages/chat (94): all pass.
  • apps/mobile typecheck has 190 errors both with and without this change (checked on a clean tree). None of them are in attachment code.
  • I did not run a desktop, mobile, or cloud box end to end.

Known limitations

  • An attachment directory that has no metadata.json (a download that failed before its metadata was written) is skipped by the sweep, not removed.
  • A recreated box imports a file again on resume and caches it twice. The plan accepts this: the index points to the newest copy, and the sweep removes the rest.
  • The plan's "store path outside the worktree" fixture for Codex and Claude goes in the next PR, because the adapters are what read those paths.

…fileId on cloud copies

Part of the attachments plan (PR 3: seams 8, 2, and the fileId carry-through
of seam 3).

The host's attachment store had no retention. Nothing deletes chat.db rows
and a terminal launch has no session, so there is no session delete to hook.
Instead, metadata.json records lastUsedAt. Upload and import set it, and every
path that hands an attachment out sets it again: read, agents.run's attached
files block, and the ACP resolver. A daily sweep (not in sandboxes) removes
entries unused for ATTACHMENT_RETENTION_DAYS. An entry written before this
change is stamped, not removed. The check and the rename that takes the
entry out of the store run in the same tick, so a hand-out on this event loop
cannot land between them. The slow delete runs async, off the loop.

A box's cached copy now keeps the fileId it was imported from, in its
metadata and in a small .by-file-id index, so the adapters (next PR) can find
a cloud part's cached copy without a scan. The first prompt of a cloud launch
spreads that metadata into its parts. Without fileId, the first attachments
of a cloud session would be the one kind a reader without the box cannot
render.

MAX_INLINE_ATTACHMENT_BYTES moves to @superset/shared/attachment-limits with
MAX_THUMBNAIL_BYTES and ATTACHMENT_RETENTION_DAYS. The host and the shared
launch schema use MAX_ATTACHMENTS for their max(10).
@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: ad9d0787-c288-41e6-8a87-624955918a86

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

Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) ✅ View Branch
Vercel API (Vercel) ✅ Open Preview
Vercel Web (Vercel) ✅ Open Preview
Vercel Marketing (Vercel) ✅ Open Preview
Vercel Docs (Vercel) ✅ Open Preview

Preview updates automatically with new commits

@greptile-apps

greptile-apps Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The changes affect attachment storage and chat launches across local hosts and cloud workspaces.

Fix the chat launch timing gap before merging so cleanup cannot delete an attachment already accepted for a prompt.

Findings

  1. P1 Chat can lose accepted attachments ▶
  2. P2 One failure stops all cleanup ▶
Fix with agent prompt
### Issue 1
packages/host-service/src/trpc/router/agents/agents.ts:669-671
`chatLaunchTarget` accepts attachments without refreshing `lastUsedAt`. The agent reads them only after its async startup finishes. If an attachment is past retention and the sweep runs during startup, the sweep deletes it before the agent reads it. The prompt then goes through without that attachment.

Use `handOutAttachment` when accepting attachments for a chat launch, as the terminal launch already does.

```suggestion
	const attachments: ChatLaunchAttachment[] = [];
	for (const attachmentId of input.attachmentIds ?? []) {
		const resolved = handOutAttachment(attachmentId);
```

### Issue 2
packages/host-service/src/trpc/router/attachments/sweep.ts:35-38
If a leftover `.sweeping-*` directory cannot be removed, the error stops the entire sweep. Every later entry waits until the next daily pass, which can fail at the same directory again. This can leave expired attachments accumulating elsewhere in the store.

Catch and log failures per entry, then continue cleaning the remaining entries.

---

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

Summary

Adds 30-day attachment cleanup, tracks last use, and carries cloud fileId values into first prompts.

  • The host clears attachment copies that have not been used for 30 days.
  • Cloud attachment IDs travel with their copies into first chat prompts.
  • Host and cloud attachment limits come from shared values.

Diagram

sequenceDiagram
  participant Launch as Chat launch
  participant Store as Attachment store
  participant Agent as Agent startup
  participant Sweep as Daily sweep
  Launch->>Store: Resolve attachment without updating last use
  Launch->>Agent: Queue first prompt
  Note over Agent: Async startup
  Sweep->>Store: Remove expired attachment
  Agent->>Store: Resolve queued attachment
  Store-->>Agent: Not found
  Note over Agent: Send prompt without attachment
Loading

Reviews (1) · Last reviewed commit: "feat(host-service): sweep the attachment..." · Reviewed by Greptile

@expo

expo Bot commented Oct 24, 2026

Copy link
Copy Markdown

Preview update published for development build db42788c (fingerprint e28b4b490ccc). Open the build on your phone and scan:

Update group f034f1a7

Comment on lines +669 to 671
const attachments: ChatLaunchAttachment[] = [];
for (const attachmentId of input.attachmentIds ?? []) {
const resolved = resolveAttachmentPath(attachmentId);

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.

P1 Chat can lose accepted attachments

chatLaunchTarget accepts attachments without refreshing lastUsedAt. The agent reads them only after its async startup finishes. If an attachment is past retention and the sweep runs during startup, the sweep deletes it before the agent reads it. The prompt then goes through without that attachment.

Use handOutAttachment when accepting attachments for a chat launch, as the terminal launch already does.

Suggested change
const attachments: ChatLaunchAttachment[] = [];
for (const attachmentId of input.attachmentIds ?? []) {
const resolved = resolveAttachmentPath(attachmentId);
const attachments: ChatLaunchAttachment[] = [];
for (const attachmentId of input.attachmentIds ?? []) {
const resolved = handOutAttachment(attachmentId);
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/host-service/src/trpc/router/agents/agents.ts
Line: 669-671

Comment:
**Chat can lose accepted attachments**

`chatLaunchTarget` accepts attachments without refreshing `lastUsedAt`. The agent reads them only after its async startup finishes. If an attachment is past retention and the sweep runs during startup, the sweep deletes it before the agent reads it. The prompt then goes through without that attachment.

Use `handOutAttachment` when accepting attachments for a chat launch, as the terminal launch already does.

```suggestion
	const attachments: ChatLaunchAttachment[] = [];
	for (const attachmentId of input.attachmentIds ?? []) {
		const resolved = handOutAttachment(attachmentId);
```

---

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

Comment on lines +35 to +38
for (const entry of entries) {
if (entry.startsWith(SWEEPING_PREFIX)) {
await rm(join(root, entry), { recursive: true, force: true });
continue;

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 One failure stops all cleanup

If a leftover .sweeping-* directory cannot be removed, the error stops the entire sweep. Every later entry waits until the next daily pass, which can fail at the same directory again. This can leave expired attachments accumulating elsewhere in the store.

Catch and log failures per entry, then continue cleaning the remaining entries.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/host-service/src/trpc/router/attachments/sweep.ts
Line: 35-38

Comment:
**One failure stops all cleanup**

If a leftover `.sweeping-*` directory cannot be removed, the error stops the entire sweep. Every later entry waits until the next daily pass, which can fail at the same directory again. This can leave expired attachments accumulating elsewhere in the store.

Catch and log failures per entry, then continue cleaning the remaining entries.

---

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

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 — a3b8f4dd Deployed Oct 24, 2026 by superset-app[bot] via Deploy API #12688
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