feat(mothership): Chat-scoped outputs/ folder in VFS + Fork/Duplicate Chat#5401
feat(mothership): Chat-scoped outputs/ folder in VFS + Fork/Duplicate Chat#5401j15z wants to merge 11 commits into
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryHigh Risk Overview Fork / duplicate expands the mothership fork route: optional Security / access: Hardening: empty resource ids rejected in API and client; stream 404 stops reconnect loops; failed chat POST rolls back optimistic state; Reviewed by Cursor Bugbot for commit 4e82a25. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryThis PR adds two related features to the chat/VFS surface: (1) a chat-scoped
Confidence Score: 5/5Safe 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
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"
%%{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"
Reviews (3): Last reviewed commit: "chore(api): bump route-count baseline fo..." | Re-trigger Greptile |
a03ff62 to
e4f59b3
Compare
|
@cursor review |
5615eff to
9d8756c
Compare
|
@cursor review |
There was a problem hiding this comment.
✅ 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.
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 workspacefiles/, living with the chat, saveable to the workspace on request; agent-side steering is behind the mothershipchat-scoped-outputsflag, 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 (upToMessageIdis 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 nullableworkspace_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):
resourcesjsonb wholesale, keeping chips for files it doesn't have; file resources whose chat-owned file wasn't copied are now dropped.live-assistant:<streamId>id and forking it 400'd; the Fork button now waits for the persisted id.generate_image/video/audioandffmpegresolvedinputs.filesworkspace-only, souploads//outputs/references never loaded — andgenerate_imagesilently generated from the prompt alone. New sharedresolveToolInputFilecovers all three VFS namespaces, and unresolvable explicit references now fail the call instead of being silently skipped.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
Testing
vitest runfromapps/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 withoutupToMessageId, 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/docstype-check noise is pre-existing — missing generated.sourceartifact; this branch touches no docs files.)outputs/; duplicates open as<name> (Copy)with every message; reference-image generation actually uses the uploaded reference.fork/route.ts(isWholeChatDuplicatedrives the message cut, file listing, title, Go body, and analytics); the ghost-resource drop rule inrewriteResourceFileRefs(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 migration0254(additive, no backfill, NULL = birth-unknown ⇒ included in every fork).Checklist
Screenshots/Videos
Companion PR
Companion (mothership): https://github.com/simstudioai/mothership/pull/342