Skip to content

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

Open
j15z wants to merge 22 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 22 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: two additive migrations — 0254 adds nullable workspace_files.message_id, the provenance column that lets a branch fork copy only the chat-owned files (uploads AND outputs) born at-or-before the cut, and 0255 adds a partial unique index on output display names (see review-response changes below).

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.

Review-response changes (e43fe9c34)

Addresses all 8 findings from code review:

  • Write-once in both modes: outputs/ + overwrite is now rejected in headless runs too — the files/ redirect can no longer silently replace a same-named workspace file.
  • Branch forks now copy pre-cut outputs (design change from review discussion): outputs are stamped with message_id at creation and join the same timeline cut as uploads, so inline embeds are re-pointed and forks fully survive source-chat deletion. The rule for every chat-owned file: it travels with the fork iff the user message that carried/requested it is kept.
  • Resolver adoption: open_resource, table import, and knowledge add-document resolve outputs//uploads/ refs and bare wf_ ids (new chat-scoped by-id fallback in resolveToolInputFile); presigning and the background CSV import honor chat-scoped storage contexts (TableImportPayload.fileContext, backward-compatible).
  • create_file guard: checks the raw fileName before files/ prefixing, so outputs/notes.md can no longer create a literal files/outputs/ folder.
  • Migration 0255: partial unique index on (chat_id, display_name) WHERE context='output' + a 23505 retry in uploadChatOutput, closing the concurrent same-name generation race (mirrors the existing uploads index/retry; separate index so the two namespaces stay independent).
  • Reader consolidation: upload-file-reader + output-file-reader merged into one context-parameterized chat-file-reader (~200 duplicated lines removed, all public names preserved); shared isOutputsPath/leaf helpers replace 5 inline prefix checks.
  • Fork blob copy: headObject replay guard (no double-copy/double-charge on retry) + bounded concurrency (4) + a single workspace_files read per fork with the branch cut applied in memory.
  • Panel actions: EmbeddedFile/EmbeddedFileActions share one resolver hook (list → by-id → chat outputs by leaf name), so Download/Open work on path-referenced outputs.

Deploy note: run a one-off duplicate check for (chat_id, display_name) output rows before applying 0255 — the unique index build fails if race-produced dupes already exist (unlikely: young, flag-gated feature).

Type of Change

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

Testing

  • Full affected sweep green (vitest run lib/copilot lib/table lib/uploads app/api/mothership from apps/sim): 107 files, 1067 tests passing, including fork route, fork-chat-files, rewrite-file-references, effective-transcript, resolve-input-file, chat-file-reader (the merged upload/output readers), vfs, materialize-file, create-file, resources, and track-chat-upload. New coverage from the review pass: write-once rejection in both interactive and headless modes, the headless files/ redirect, the create_file raw-fileName guard, the wf_ id chat-scoped fallback, outputs joining the fork timeline cut (pre-cut copied + re-pointed, post-cut ghosts dropped), the blob-copy replay guard, and the bounded concurrency pool — alongside the earlier whole-chat duplicate, ghost-resource, and 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 carry pre-cut outputs with re-pointed embeds and leave post-cut files behind; 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 — now a single workspace_files read cut in memory by filterForkableChatFiles); 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); the write-once check ordering in resource-writer.ts (rejection must precede the headless redirect); and the two migrations (0254: additive, no backfill, NULL = birth-unknown ⇒ included in every fork; 0255: partial unique index, see deploy note).

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.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 6, 2026 3:11am

Request Review

@cursor

cursor Bot commented Jul 3, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Touches auth for a new storage context, billing quota on fork copies, and large fork/blob-copy paths with documented crash and refund gaps; client stream/resource changes affect core chat reliability.

Overview
Introduces chat-scoped outputs/ (alongside uploads) end-to-end: merged chat-file-reader, GET …/chats/[chatId]/outputs, storageContext: 'output' on records, and UI hooks so the resource picker, tabs, and /api/files/view can list, resolve, and preview outputs that never appear in the workspace Files list. materialize_file can promote uploads or outputs into workspace files (with name disambiguation); tool writes thread chatId / interactive / messageId so interactive outputs/ paths don’t silently land in files/.

Fork and duplicate share one route: optional upToMessageId for branch vs whole-chat copy, quota gate on copyable bytes, planChatFileCopies + post-commit blob copies (quota-charged, concurrency-bounded), transcript/resource rewrites, ghost resource drops after a branch cut, and cleanup when blob copy fails. Sidebar Duplicate and message Fork surface failedFileCopies warnings; fork is disabled on live assistant message ids until persisted.

Security & reliability: output files require chat owner, not just workspace membership, on serve/view paths; stream resume returns 503 on DB lookup failure instead of 404 (avoids terminal reconnect wedge). Chat client hardens resource ids (reject empty, filter placeholders, legacy cleanup) and pre-stream send failures (rollback + no bogus reconnect). Contracts sync mothership/copilot resource types; ISSUES.md documents known follow-ups (quota refund, delete orphan blobs, fork OOM window).

Reviewed by Cursor Bugbot for commit 4a2333a. 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 major capabilities to the chat/VFS surface: (1) a chat-scoped outputs/ namespace for agent-generated one-off files (images, audio, video), and (2) Fork and Duplicate — a single fork endpoint driving two gestures. It also ships a batch of field fixes found during testing (silent reference-image skip, phantom-stream reconnect wedge, ghost file resources on forks, live-message fork 400).

  • Chat-scoped outputs (outputs/<name>, context='output'): write-once, flat, tagged to the owning chat via FK. Two migrations: 0254 adds nullable workspace_files.message_id for timeline-cut on branch forks; 0255 adds a CONCURRENTLY-built partial unique index on (chat_id, display_name) WHERE context='output' with a 23505 retry in uploadChatOutput, mirroring the existing uploads index.
  • Fork / Duplicate: one POST endpoint, two modes (upToMessageId set → branch fork titled "Fork | …", omitted → whole-chat duplicate titled "… (Copy)"). A single workspace_files read per fork, cut in memory by filterForkableChatFiles. Blob copies are bounded-concurrency, best-effort post-commit; failed copies clean up their dead DB rows, surface failedFileCopies in the response, and show a toast warning. Ghost file resources (uploads/outputs not copied in a branch fork) are dropped from the new chat's resource chips.
  • Resolver consolidation: upload-file-reader + output-file-reader merged into a single context-parameterized chat-file-reader; a new resolveToolInputFile covers all three VFS namespaces (files/, uploads/, outputs/) and bare wf_ IDs; generate_image, generate_video, generate_audio, and ffmpeg now fail instead of silently skipping an unresolvable explicit reference input.

Confidence Score: 5/5

Safe to merge. The fork/duplicate logic, authorization changes, and outputs namespace are well-structured, consistently applied, and backed by thorough test coverage. The two migrations are additive and carefully constructed.

No data-correctness or security defects were found. The authorization additions — ownership check for output files in both verifyWorkspaceFileAccess and verifyRegularFileAccess, WORKSPACE_FILE_LOOKUP_CONTEXTS filter, getPreviewableWorkspaceFile userId guard — are consistently applied and correct. The fork transaction correctly rolls back on any failure; post-commit blob copies are best-effort with proper dead-row cleanup and user-visible warnings. The rewrite functions correctly handle the ghost-resource drop and never leave cross-chat dangling references. The only findings are a missing 'output' entry in the schema comment and a missing isPending guard on the Duplicate context-menu action.

No files require special attention. The schema.ts context comment omission and the sidebar double-click guard are the only items worth addressing before shipping.

Important Files Changed

Filename Overview
apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts Implements the unified fork/duplicate endpoint. Two-mode branch (isWholeChatDuplicate) correctly drives message cut, file listing, title, Go body, and analytics from a single workspace_files read. Failed blob copies clean up dead rows and return failedFileCopies to the UI. Logic is sound.
apps/sim/lib/copilot/chat/fork-chat-files.ts planChatFileCopies inserts copy rows in-transaction then defers blob I/O. filterForkableChatFiles correctly includes NULL-messageId rows in every fork. executeChatFileBlobCopies uses bounded concurrency (4) and best-effort error handling with per-task failure tracking.
apps/sim/lib/copilot/chat/rewrite-file-references.ts rewriteMessageFileRefs and rewriteResourceFileRefs correctly use idMap/keyMap for rewrites and drop ghost file resources (chat-owned but not copied) via the dropFileIds set. Pure functions, well-tested.
apps/sim/lib/copilot/vfs/resource-writer.ts isOutputsPath guard applied before files/ prefix (preventing outputs/notes.md → files/outputs/ escape). Write-once rejection now precedes headless redirect. Correct chatId + interactive gate for uploadChatOutput calls.
apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts uploadChatOutput follows the same retry+cleanup pattern as trackChatUpload. mapWorkspaceFileRecord now propagates the real storageContext instead of hardcoding 'workspace'. getPreviewableWorkspaceFile adds correct userId ownership check for output rows. Logic is sound.
apps/sim/app/api/files/authorization.ts lookupWorkspaceFileByKey now covers both 'workspace' and 'output' contexts. outputOwnershipSatisfied check is applied in both verifyWorkspaceFileAccess and verifyRegularFileAccess, correctly preventing cross-user output access by workspace membership alone.
apps/sim/lib/copilot/tools/handlers/chat-file-reader.ts Merges upload-file-reader and output-file-reader into a single parameterized namespace. resolveChatFileRecordById correctly scopes to chatId + context IN ('mothership','output'). Normalized name comparison handles encoded vs raw segment variants.
packages/db/schema.ts Adds nullable message_id text column and chatOutputDisplayNameUnique partial unique index. The context column inline comment still lists only the original contexts and omits 'output', which is now a first-class context written by uploadChatOutput.
packages/db/migrations/0255_chat_output_display_name_unique.sql Uses CONCURRENTLY + explicit COMMIT (runner convention) to avoid ACCESS EXCLUSIVE on the live table. Deploy note in PR description calls out the pre-migration duplicate check requirement. Pattern mirrors the existing uploads index migration.
apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts isPersistedChatResource predicate consistently filters empty-id and streaming-file placeholders from send, reorder, and persist paths. StreamNotFoundError breaks the reconnect loop for 404 runs immediately. streamStarted gate correctly prevents rollback of a never-registered stream.
apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx handleDuplicateChat wires useForkMothershipChat for the right-click Duplicate action and shows a toast for partial file-copy failures. Missing forkChatMutation.isPending guard could allow concurrent duplicate requests on rapid double-click.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[POST /fork chatId] --> B{upToMessageId set?}
    B -- No --> C[Whole-chat duplicate\nAll files, title: name Copy]
    B -- Yes --> D[Branch fork\nSlice messages to cut point\ntitle: Fork name]

    C --> E[listDuplicableChatFiles\nALL chat-owned files\ncontext IN mothership,output]
    D --> F[listDuplicableChatFiles then filterForkableChatFiles\nmessageId IN keptSet OR NULL]

    E --> G[checkStorageQuota]
    F --> G

    G --> H[DB Transaction: INSERT copilot_chats]
    H --> I[planChatFileCopies\nINSERT workspace_files copies\nfresh id + key per row]
    I --> J[rewriteResourceFileRefs\nremap chips to new ids\ndrop ghost chat-owned refs]
    J --> K[rewriteMessageFileRefs\nremap transcript URLs and attachments]
    K --> L[appendCopilotChatMessages in tx]
    L --> M[COMMIT]

    M --> N[executeChatFileBlobCopies\nbounded concurrency 4\nbest-effort post-commit]
    N --> O{Any failed?}
    O -- Yes --> P[DELETE dead workspace_files rows\nremoveChatResources\nreturn failedFileCopies in response]
    O -- No --> Q[Fork copilot-service Go endpoint\nbest-effort]
    P --> Q

    Q --> R[publishStatusChanged chatPubSub]
    R --> S[Return id and optional failedFileCopies]
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"}}}%%
flowchart TD
    A[POST /fork chatId] --> B{upToMessageId set?}
    B -- No --> C[Whole-chat duplicate\nAll files, title: name Copy]
    B -- Yes --> D[Branch fork\nSlice messages to cut point\ntitle: Fork name]

    C --> E[listDuplicableChatFiles\nALL chat-owned files\ncontext IN mothership,output]
    D --> F[listDuplicableChatFiles then filterForkableChatFiles\nmessageId IN keptSet OR NULL]

    E --> G[checkStorageQuota]
    F --> G

    G --> H[DB Transaction: INSERT copilot_chats]
    H --> I[planChatFileCopies\nINSERT workspace_files copies\nfresh id + key per row]
    I --> J[rewriteResourceFileRefs\nremap chips to new ids\ndrop ghost chat-owned refs]
    J --> K[rewriteMessageFileRefs\nremap transcript URLs and attachments]
    K --> L[appendCopilotChatMessages in tx]
    L --> M[COMMIT]

    M --> N[executeChatFileBlobCopies\nbounded concurrency 4\nbest-effort post-commit]
    N --> O{Any failed?}
    O -- Yes --> P[DELETE dead workspace_files rows\nremoveChatResources\nreturn failedFileCopies in response]
    O -- No --> Q[Fork copilot-service Go endpoint\nbest-effort]
    P --> Q

    Q --> R[publishStatusChanged chatPubSub]
    R --> S[Return id and optional failedFileCopies]
Loading

Reviews (9): Last reviewed commit: "test(contracts): pin mothership↔copilot ..." | Re-trigger Greptile

Comment thread apps/sim/lib/copilot/chat/fork-chat-files.ts Outdated
Comment thread apps/sim/hooks/queries/workspace-files.ts Outdated
@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.

Fixes from the multi-agent review of #5401 (all subagent-verified, plus
Cursor Bugbot's queued-send finding):

- Thread chatId/interactive/messageId through the generic tool-output
  writer (run_code/user_table outputs.files), so interactive outputs/
  writes become chat-scoped outputs instead of silently landing in
  workspace files/ while the agent references a nonexistent outputs/ path
- Stream resume: reserve 404 for a true miss — run-lookup failures now
  return 503 so a transient DB error can't permanently kill reconnection
  (client treats 404 as terminal by design)
- materialize_file save: accept uploads//outputs/ prefixes and error on
  bare-name ambiguity instead of silently promoting the upload
- open_resource: reject chat uploads (no client surface can preview
  mothership rows — prevents permanently-broken resource tabs)
- uploadChatOutput: delete the just-uploaded blob when the row insert
  never lands (no more orphaned bytes invisible to chat cleanup); replace
  the per-candidate name pre-SELECT with one names read + 23505 retry
- knowledge add_file: chat-scoped refs only resolve into KBs in the
  chat's own workspace (matches open_resource's boundary)
- resolveToolInputFile: uploads//outputs/ refs fall through to the
  workspace resolver when no chatId exists; reader tries the decoded
  name spelling in the indexed query (no more guaranteed full-scan for
  encoded names)
- resource-writer: outputs leaf = FIRST path segment (readers' rule);
  write-once error names the files/outputs/ spelling for one-turn recovery
- rename/delete/move tools: explicit policy errors for chat-scoped paths
  (and no more resolving 'outputs/x' against a real files/outputs folder)
- vfs: unified chat-scoped read branch (restores the dropped guidance
  sentence), grep leaf via chatScopedLeafSegment
- fork: remove the dead headObject guard (no replay path exists here),
  swap the hand-rolled pool for mapWithConcurrency, batch the copy-row
  insert, drop the dead keyMap clause
- use-chat: rolled-back sends return false so queued messages are
  restored (Cursor finding); hydration deletes legacy empty-id resource
  rows server-side (unbreaks reorder); shared isPersistedChatResource
  predicate
- preview mapping carries the row's real storageContext (fixes serve
  context for by-id output previews); hide Open-in-Files for outputs;
  byId query-key factory entry; Set-based dedup in the resource dropdown

Adds ISSUES.md: verified findings not yet fixed on this branch (quota
refund design, interactive-delete blob cleanup, fork durability/memory,
>100MB fork copies, chat-scoped glob pattern filtering, sandbox-export
outputs threading, namespace shadowing, home.tsx click drop, and the
consolidated-bucket purge edge), each with mechanism and fix design.

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

j15z commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

@j15z

j15z commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author

@cursor review

- The hydration cleanup of legacy empty-id resource rows was a silent
  client-side no-op: requestJson validates the outbound body, and the
  mothership remove contract still required resourceId.min(1) while only
  its copilot twin was permissive. Relax the mothership schema to match
  (the shim route delegates to the copilot handler, which already
  tolerates empty ids) so the cleanup actually reaches the server and
  reorder unbreaks.
- uploadChatOutput's orphan-blob cleanup could delete a COMMITTED row's
  bytes on a commit-ack race (insert commits, connection resets before
  the ack, driver throws, displayName stays null). Verify no row
  references the storage key before deleting; if the check fails, prefer
  an orphaned blob over a broken live file.

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

j15z commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

@j15z

j15z commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/home/home.tsx
Comment thread apps/sim/lib/copilot/tools/handlers/materialize-file.ts
import only resolved chat uploads while save was extended to both
namespaces — agent-generated workflow JSON under outputs/ always
returned upload-not-found. Same routing as save: uploads//outputs/
prefixes target a namespace, bare-name collisions error as ambiguous.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts
The mothership resource routes are shims delegating to the copilot
handlers, so the two contract families must describe the same boundary:

- copilotResourceTypeSchema was missing filefolder/task/integration/
  generic — valid MothershipResourceType rows of those types 400'd at the
  delegated parse, silently re-breaking the legacy empty-id cleanup (and
  reorder) for chats holding them. Enum now covers every member.
- mothership add/reorder REQUEST items now require a non-empty id like
  the delegated copilot schemas (the client contract was advertising
  acceptance of payloads the server rejects); RESPONSE items stay
  permissive so replies containing not-yet-cleaned legacy rows still
  validate.

Also records the confirmed _context-whitelist mechanism on ISSUES.md
item 7 (sandbox-export chat threading).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
Comment thread apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts Outdated
The mothership resource routes delegate to the copilot handlers, so the
two contract families describe one physical boundary. These tests turn
the next one-sided schema edit into a red test instead of a silent
client-side ZodError or delegated 400: the copilot resource-type enum
must cover every MothershipResourceType, remove stays permissive on both
sides (legacy empty-id deletion), add/reorder reject empty ids on both
sides, and mothership responses stay permissive for not-yet-cleaned
legacy rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/sim/lib/copilot/chat/fork-chat-files.ts
@j15z

j15z commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

@j15z

j15z commented Jul 6, 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!

5 issues from previous reviews remain unresolved.

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit 8afecd5. Configure here.

Round-4 review: the enum widening fully fixed remove/reorder (and made
integration adds work), but POST keeps its own narrower
VALID_RESOURCE_TYPES allow-list — scope the comment so it doesn't
overclaim, ledger the pre-existing filefolder-add 400 as ISSUES.md item
11, and fix a stale 'from upload' log line in materialize import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/sim/hooks/queries/workspace-files.ts Outdated
…eviews

Cursor auto-reviews each push; four findings landed between polling
windows and surfaced via unresolved threads:

- useChatOutputs no longer swallows list failures into a successful [] —
  errors throw so React Query retries instead of caching "no outputs"
  for the stale window (masked path-based output link resolution)
- flushPendingResources and the post-flush reorder now use the shared
  isPersistedChatResource predicate (empty-id resources could reach the
  tightened API validation via the flush path)
- fork quota gate only counts rows the plan will copy (workspaceId-less
  legacy rows are skipped by planChatFileCopies but their bytes could
  reject an otherwise-in-quota fork)
- duplicate titles strip a leading "Fork | " like branch forks do:
  duplicating a forked chat yields "Name (Copy)", not
  "Fork | Name (Copy)"

The fifth finding (copied-counter "race") is refuted on-thread: JS is
single-threaded and the increment has no await between read and write.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/sim/hooks/queries/mothership-chats.ts Outdated
Comment thread apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
- Optimistic duplicate titles strip "Fork | " like the server now does
  (no brief sidebar rename on refetch)
- The hydration localOnly merge uses isPersistedChatResource — the last
  bare-predicate site that could readmit empty-id local resources

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

@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.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4a2333a. Configure here.

error: toError(err).message,
})
return []
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Outputs API hides list failures

Medium Severity

The new chat-outputs HTTP route calls listChatOutputs, which catches database errors and returns an empty array. The route then responds with success: true and files: [], so transient DB failures look like “no outputs” instead of a 500. That undermines useChatOutputs throwing on failure, because the client never sees an error to retry.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4a2333a. Configure here.

const isResolving =
isLoading ||
(isFetching && !file) ||
(listSettled && !listFile && (fallbackLoading || outputsLoading))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Errored outputs query shows not found

Medium Severity

After useChatOutputs fails (network or 500), callers still default data to [] and never read isError. useResolvedEmbeddedFile then stops resolving once list and by-id lookups miss, so the embedded preview shows “File not found” instead of loading or error/retry—even when outputs exist.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4a2333a. 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