Skip to content

feat(mothership): Chat-scoped outputs/ folder in VFS + Fork/Duplicate Chat#5401

Open
j15z wants to merge 11 commits into
devfrom
feat/chat-scoped-outputs-and-duplicate
Open

feat(mothership): Chat-scoped outputs/ folder in VFS + Fork/Duplicate Chat#5401
j15z wants to merge 11 commits into
devfrom
feat/chat-scoped-outputs-and-duplicate

Conversation

@j15z

@j15z j15z commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds two related capabilities on the chat/VFS surface, plus the field fixes found while testing them. (1) A chat-scoped outputs/ namespace for one-off generated files (images, audio, video, ffmpeg results) — separate from workspace files/, living with the chat, saveable to the workspace on request; agent-side steering is behind the mothership chat-scoped-outputs flag, so it dark-launches. (2) Fork and Duplicate — one engine, two gestures. The Fork button on each assistant reply copies the conversation up to and including that message into a fresh chat (Fork | <name>); the right-click Duplicate action is the same fork route with no cut point (upToMessageId is now optional) — a whole-chat copy titled <name> (Copy) that keeps every message and copies agent outputs too. In both modes, copied files get fresh row ids and storage keys, bytes are physically copied and quota-charged, and every in-transcript file reference is re-pointed at the copies so the copy survives deletion of the original. Duplicates ask the mothership fork endpoint for its whole-chat clone mode (compacted working memory preserved verbatim); branch forks keep the truncate-and-rebuild path. Schema: one additive migration (0254) adds nullable workspace_files.message_id, the provenance column that lets a branch fork copy only the uploads born at-or-before the cut.

Field fixes included (found via live testing of the above):

  • Ghost resources: a branch fork copied the chat's resources jsonb wholesale, keeping chips for files it doesn't have; file resources whose chat-owned file wasn't copied are now dropped.
  • Fork on a live message: a just-streamed reply carries a synthetic live-assistant:<streamId> id and forking it 400'd; the Fork button now waits for the persisted id.
  • Media-tool reference inputs: generate_image/video/audio and ffmpeg resolved inputs.files workspace-only, so uploads//outputs/ references never loaded — and generate_image silently generated from the prompt alone. New shared resolveToolInputFile covers all three VFS namespaces, and unresolvable explicit references now fail the call instead of being silently skipped.
  • Phantom-stream reconnect wedge: an outputs/ link click on the home surface minted an empty-id resource that 400'd the next send and wedged the UI in "running" for ~3 minutes; empty-id resources are now rejected and a failed POST rolls back instead of retry-reconnecting.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Other: ___________

Testing

  • Targeted suites green (vitest run from apps/sim): fork route, fork-chat-files, rewrite-file-references, effective-transcript, resolve-input-file, upload/output-file-reader, vfs, materialize-file, track-chat-upload → 10 files, 101 tests passing. New coverage: whole-chat duplicate (all messages kept, both file contexts copied, (Copy) title, Go body without upToMessageId, quota over upload+output bytes, empty-chat duplicate, empty-string id still 400s), ghost-resource regressions (uncopied output resource dropped; ghosts dropped even when zero files copy; duplicates keep and re-point everything), and chat-scoped resolver cases.
  • tsc --noEmit ✅ 0 errors · biome check ✅ clean · check:api-validation ✅ · check:react-query ✅. (apps/docs type-check noise is pre-existing — missing generated .source artifact; this branch touches no docs files.)
  • Manual (local dev, three rounds of field-testing): uploads copy with their bytes and re-pointed references; branch forks start with clean outputs/; duplicates open as <name> (Copy) with every message; reference-image generation actually uses the uploaded reference.
  • Reviewers should focus on: the two-mode branch in fork/route.ts (isWholeChatDuplicate drives the message cut, file listing, title, Go body, and analytics); the ghost-resource drop rule in rewriteResourceFileRefs (chat-owned ∧ not-copied ⇒ dropped; shared workspace files pass through); the deliberate quota divergence from the workspace-fork precedent (forked bytes ARE counted — see the comment at the increment site); and migration 0254 (additive, no backfill, NULL = birth-unknown ⇒ included in every fork).

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Screenshots/Videos

Companion PR

Companion (mothership): https://github.com/simstudioai/mothership/pull/342

Justin Blumencranz and others added 5 commits July 1, 2026 18:04
Adds Duplicate to the chat context menu. POST /api/mothership/chats/
[chatId]/duplicate clones the chat row, all messages, and the chat-owned
files (uploads + outputs) under new ids/keys, rewriting every in-transcript
file reference (attachment chips, embedded serve/view URLs, context chips,
file resources) so the copy survives deletion of the original. Copied bytes
are quota-checked up front and counted on success — deliberately diverging
from the workspace-fork precedent (see comment at the increment site).
Agent-side conversation state clones best-effort via the Go fork endpoint's
new whole-chat mode. Duplicating navigates into the copy, titled
"<name> (Copy)".

Also contract-binds the vfs outputs route (surfaced by check:api-validation):
listChatOutputsContract, storageContext enum + folderId alignment, and
useChatOutputs upgraded from raw fetch to requestJson.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sidebar Duplicate action is replaced by a Fork button on each assistant
reply (next to feedback). Forking copies the conversation up to and including
the clicked message, plus the chat's uploads born at-or-before that point:
each copy gets a fresh row id and storage key, the same message_id, physically
copied bytes counted against the storage quota, and every in-transcript file
reference re-pointed at the copies. Agent outputs/ stay behind.

- workspace_files gains a nullable message_id provenance column (drizzle
  migration 0254); trackChatUpload stamps it from the sending user message
- fork route gains the quota gate + file copy + reference rewrite, reusing
  the machinery built for duplicate (fork-chat-files.ts, rewrite helper)
- materialize_file nulls message_id alongside chatId
- duplicate route/contract/hook/tests removed; sidebar Duplicate reverted to
  its pre-branch disabled state (showDuplicate={false})

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clicking a #wsres-file outputs/ link minted a resource with an empty id
(the outputs lookup ran with the route chatId, which stays undefined on
the home surface), which was persisted and attached to the next send.
The chat POST then 400'd on attachment validation before creating a run,
and the send's catch "recovered" by reconnecting to its own never-
registered stream id — 10 backoff retries against stream_not_found,
~3 minutes of stuck "running" UI with the real error swallowed.

- resolve outputs/ file links with the stream-resolved chat id, and drop
  file resources that still have no id after resolution
- reject empty-id resources in addResource, hydration merge, and the
  send's resourceAttachments
- only retry-reconnect when the stream actually started; a failed POST
  now rolls back the optimistic send and surfaces the error
- treat resume 404 (stream_not_found) as terminal instead of retrying
- require min(1) resource ids in the add/reorder contracts (remove stays
  permissive so legacy empty-id rows can be deleted)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Jul 4, 2026 12:05am

Request Review

@cursor

cursor Bot commented Jul 3, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes span file authorization (privacy leak fix), fork transactions with blob copies and quota, and broad chat/stream client state — mistakes could leak outputs across members or leave broken forks/resources.

Overview
Introduces a chat-scoped outputs/ namespace for agent-generated one-offs (parallel to uploads/), wired through VFS read/glob/grep, GET /api/mothership/chats/[chatId]/outputs, UI listing in the resource picker, and writeWorkspaceFileByPath / media tools when outputs/<name> is requested on interactive turns. materialize_file can promote outputs to workspace files/ with name disambiguation.

Fork / duplicate expands the mothership fork route: optional upToMessageId (branch vs whole-chat duplicate), physical file row + blob copies with quota checks, transcript/resource reference rewrites, ghost resource dropping, failed-copy cleanup, and sidebar Duplicate plus enabled message Fork (blocked on live assistant ids).

Security / access: output files require workspace membership and row owner; DB lookup includes output context so serve/view works; /api/files/view allows embedding output files.

Hardening: empty resource ids rejected in API and client; stream 404 stops reconnect loops; failed chat POST rolls back optimistic state; resolveToolInputFile for media reference paths; upload message_id stamping for timeline-aware fork cuts; chat cleanup includes output context.

Reviewed by Cursor Bugbot for commit 4e82a25. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
Comment thread apps/sim/lib/copilot/tools/handlers/materialize-file.ts
@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds two related features to the chat/VFS surface: (1) a chat-scoped outputs/ namespace for agent-generated files (images, audio, video, ffmpeg results) that live with the chat and can optionally be saved to the workspace, and (2) Fork and Duplicate — one engine that either branches a conversation at a message cutpoint or deep-copies the whole chat including outputs. It also bundles several field fixes found during live testing: ghost resources on branch forks, a live-message fork 400, media tool input resolution across all VFS namespaces, and the phantom-stream reconnect wedge.

  • Schema change (0254): additive nullable workspace_files.message_id column that lets a branch fork copy only uploads born at-or-before the cut point; NULL rows are conservatively included in every fork.
  • New storage context (output): uploadChatOutput writes agent-generated files to workspace_files with context='output', added to cleanup scopes and authorization paths with an ownership guard so workspace membership alone cannot serve another member's chat outputs.
  • Two-mode fork route: isWholeChatDuplicate drives the message cut, file listing, title, Go body, and analytics; blob copies are best-effort post-transaction with dead-row cleanup on failure.

Confidence Score: 5/5

Safe to merge; the new output context authorization is backed by ownership checks and tests, the fork transaction is atomic with post-commit best-effort cleanup, and the schema change is purely additive.

The two-mode fork route is well-structured: quota gate before the transaction, atomic row plan inside it, and dead-row cleanup after blob failures. The output context ownership rule is applied on both the explicit-context serve path and the inferred-workspace path, covered by authorization tests. The message_id column is nullable and backward-compatible. The three flagged observations are an incomplete write-once mode guard, an advisory name dedup without a DB constraint, and a missing query invalidation — none affect data integrity or security.

resource-writer.ts (write-once guard covers only overwrite, not create-or-update) and workspace-file-manager.ts (advisory name dedup for outputs has no backing DB constraint).

Important Files Changed

Filename Overview
apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts Two-mode fork/duplicate route; quota gate, transactional file row plan, post-commit best-effort blob copy with dead-row cleanup. Well-structured but outputs/ write-once enforcement is incomplete.
apps/sim/lib/copilot/chat/fork-chat-files.ts New file: listForkableChatFiles/listDuplicableChatFiles/planChatFileCopies/executeChatFileBlobCopies implement per-chat file copying with idMap/keyMap for reference rewriting. Well-tested.
apps/sim/lib/copilot/chat/rewrite-file-references.ts New file: rewrites message file refs and resource file refs using idMap/keyMap; drops ghost chat-owned resources not copied in a branch fork.
apps/sim/lib/copilot/tools/handlers/output-file-reader.ts New file: read/list/grep for chat-scoped output files; mirrors upload-file-reader.ts with VFS name normalization and most-recent tie-breaking.
apps/sim/lib/copilot/vfs/resource-writer.ts Adds outputs/ dispatch: rejects overwrite mode, calls uploadChatOutput for interactive chats, transparently redirects to files/ for non-interactive runs. create-or-update is not rejected.
apps/sim/app/api/files/authorization.ts Routes output context to verifyWorkspaceFileAccess; adds outputOwnershipSatisfied check; WORKSPACE_FILE_LOOKUP_CONTEXTS extended to include output; backed by new tests.
apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts Adds uploadChatOutput (write-only, advisory name dedup), getPreviewableWorkspaceFile (output ownership guard), messageId param added to trackChatUpload.
apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts Fixes phantom-stream reconnect wedge (streamStarted flag, StreamNotFoundError), empty-id resource guard, rollback on pre-stream POST failure.
apps/sim/lib/copilot/tools/server/image/generate-image.ts Switches to resolveToolInputFile covering uploads/ and outputs/; unresolvable explicit references now fail the call rather than silently proceeding.
packages/db/schema.ts Additive nullable message_id column on workspaceFiles; no FK by design; migration 0254 is a single ALTER TABLE ADD COLUMN with no backfill.
apps/sim/lib/cleanup/chat-cleanup.ts Correctly extends CHAT_SCOPED_CONTEXTS to include output so agent-generated outputs are deleted with the owning chat.
apps/sim/lib/copilot/tools/server/files/resolve-input-file.ts New unified resolver for tool input paths covering uploads/, outputs/, and workspace files/ namespaces.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as UI (Sidebar/MessageActions)
    participant ForkAPI as POST /chats/[id]/fork
    participant DB as Database (Drizzle TX)
    participant Blob as Storage (S3/Local)
    participant GoAPI as Go Copilot Service

    UI->>ForkAPI: "{upToMessageId?} (omit = duplicate)"
    ForkAPI->>DB: load messages, list source files
    ForkAPI->>ForkAPI: quota gate (sum sizes)
    ForkAPI->>DB: BEGIN TX
    DB->>DB: INSERT copilot_chats (new row)
    DB->>DB: INSERT workspace_files (copy rows, fresh id+key)
    DB->>DB: rewriteResourceFileRefs (drop ghosts)
    DB->>DB: appendCopilotChatMessages (rewritten refs)
    DB->>DB: COMMIT
    ForkAPI->>Blob: executeChatFileBlobCopies (best-effort)
    alt blob copy fails
        ForkAPI->>DB: DELETE dead workspace_files rows
        ForkAPI->>DB: removeChatResources (broken chips)
    end
    ForkAPI->>GoAPI: POST /api/chats/fork (clone service state)
    ForkAPI-->>UI: "{id, failedFileCopies?}"
    UI->>UI: "navigate to new chat, show warning if failedFileCopies > 0"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant UI as UI (Sidebar/MessageActions)
    participant ForkAPI as POST /chats/[id]/fork
    participant DB as Database (Drizzle TX)
    participant Blob as Storage (S3/Local)
    participant GoAPI as Go Copilot Service

    UI->>ForkAPI: "{upToMessageId?} (omit = duplicate)"
    ForkAPI->>DB: load messages, list source files
    ForkAPI->>ForkAPI: quota gate (sum sizes)
    ForkAPI->>DB: BEGIN TX
    DB->>DB: INSERT copilot_chats (new row)
    DB->>DB: INSERT workspace_files (copy rows, fresh id+key)
    DB->>DB: rewriteResourceFileRefs (drop ghosts)
    DB->>DB: appendCopilotChatMessages (rewritten refs)
    DB->>DB: COMMIT
    ForkAPI->>Blob: executeChatFileBlobCopies (best-effort)
    alt blob copy fails
        ForkAPI->>DB: DELETE dead workspace_files rows
        ForkAPI->>DB: removeChatResources (broken chips)
    end
    ForkAPI->>GoAPI: POST /api/chats/fork (clone service state)
    ForkAPI-->>UI: "{id, failedFileCopies?}"
    UI->>UI: "navigate to new chat, show warning if failedFileCopies > 0"
Loading

Reviews (3): Last reviewed commit: "chore(api): bump route-count baseline fo..." | Re-trigger Greptile

Comment thread apps/sim/lib/copilot/chat/fork-chat-files.ts Outdated
Comment thread apps/sim/hooks/queries/workspace-files.ts
@j15z j15z force-pushed the feat/chat-scoped-outputs-and-duplicate branch from a03ff62 to e4f59b3 Compare July 3, 2026 23:31
@j15z

j15z commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

@j15z

j15z commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/files/authorization.ts
@j15z j15z force-pushed the feat/chat-scoped-outputs-and-duplicate branch from 5615eff to 9d8756c Compare July 3, 2026 23:54
@j15z

j15z commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

@j15z

j15z commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 9d8756c. Configure here.

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.

1 participant