refactor(server): threads without a project run through a ScratchWorkspace service - #14521
Conversation
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: maria-rcks <maria@kuuro.net> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ad busy (#14497) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merges origin/main (1905846) into t3code/codex-turn-mapping (f3eb99e). Brings in #14375, #10963, #14351, #14462 (vite-plus 1.0), #14479, #14480, #14485, #14486, #14490, #13612 (threads without a project), #14497 and #14499. No new migrations on main. V1 files V2 deleted (kept deleted): - provider/Layers/ClaudeAdapter(.test).ts: #14497 ignores a result that answers a different Claude turn (an echoed user_message_uuid(s) that is not the active turn's, or a non-human origin), so a /compact queued behind a Claude-initiated turn gets its own result. V2 covers part of this. ClaudeAdapterV2.ts:5913 drops the zero-turn task-notification result that #14497's test sends, and :6178 sends a positive-turn task-notification wake to a continuation run once the process echoes early. Two cases are not covered: a positive-turn task-notification wake before the process's first early echo is accepted as the prompt's result (:6161, :5945), and V2 checks only origin "task-notification" (:2289), not other non-human origins. Both are left for a separate fix in ClaudeAdapterV2, not ported here. - server.test.ts: #13612's tests for the V1 WebSocket dispatch path. The stacked ScratchWorkspace service PR adds service tests instead. Content conflicts: - relay/AgentAwarenessRelay.test.ts: V2's tests with #14462's vite-plus 1.0 API (describe.sequential -> describe(name, { concurrent: false }, ...)). V2 has no signRelayAgentActivityPublishProof block, so only startup catch-up changes. - web performance.bench.ts and client-runtime remotePerformance.bench.ts: V2's benchmarks (V2 projection fixtures, message updates) in #14462's test(name, ({ bench }) => bench(...).run(options)) form. - KeybindingsSettings.logic.test.ts: V2's order (usage.open ranked first, checked against both binding orders). Main's compareCommands and chat.newWithoutProject merged cleanly into the logic and defaults. - contracts/rpc.ts: both V2's projects.mutate and #13612's projects.ensureScratch (main's contract shape), with both import sets. - ws.ts: V2's side everywhere. #13612's V1 dispatch additions are not brought back. projects.ensureScratch is a stub that fails with "Threads without a project are not available yet.", and the config answer omits scratchWorkspaceRoot, so web and mobile do not offer the feature. The stacked ScratchWorkspace service PR implements both. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…space service #13612 put "start threads without a project" inline in the WebSocket handler, so only clients could reach it. A ScratchWorkspace service now owns it: - root: the Scratch folder, offered only when the data dir is outside a Git checkout (probed once, re-probed after an interrupt). - ensureProject: one "No project" project per environment, built on ProjectService.bootstrap; a racing create resolves to the winner from its ProjectConflictError. The folder is re-made on every call. - folderForThread: a fresh date-words-id folder for a Scratch thread. ThreadLaunchService claims that folder for a Scratch thread launched at the project root and binds it like an existing worktree, so web, mobile, MCP launches and scheduled tasks all get one. projects.ensureScratch and the config answer call the service, and t3_thread_launch takes scratch: true. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This change adds a cross-cutting ScratchWorkspace capability with persistent project and filesystem provisioning, new MCP and WebSocket behavior, and retry-aware orchestration integration. Its substantial new service and runtime surface go beyond a bounded additive option. You can add or adjust custom eligibility rules. Learn more. |
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: unavailable · PR result: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
Thread ids that normalize to the same characters, or a launch retried without its receipt, could find both the short and full folder names taken, and the second thread was handed the existing folder. It now claims a fresh suffixed folder. Also exports make, and constructs ScratchWorkspaceProjectError at each failure boundary. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Nothing composes the service from make directly, so knip flags the export. The layer stays the only entry point. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#13612 started threads without a project from inline code in the
ws.tsWebSocket handler. Agents, scheduled tasks and the MCP server couldn't reach it, and only the client's first-turn path got a per-thread folder. This moves it into aScratchWorkspaceservice, so every V2 launch path, and MCP, goes through one place.Stacked on #14504 (the main merge, which stubs
projects.ensureScratch). Both merge together.Design
apps/server/src/project/ScratchWorkspace.ts, aContext.Servicewithmake/layer, imported as a namespace everywhere:root: Effect<Option<string>>: the folder at<baseDir>/scratch. It is offered only when the data dir is outside a Git checkout. Like feat: start threads without a project #13612, it's probed once and cached, and an interrupted probe clears the cache.ensureProject: Effect<{ projectId }, ScratchWorkspaceError>: creates one "No project" project per environment onProjectService.bootstrap, with the dashed-bubble icon set only at create.bootstraplooks up the root before it takes the workspace lock, so concurrent first calls do race. The loser'sProjectConflictErrornames the winner, and the service returns that project.folderForThread({ projectId, threadId, text }): Some only for the Scratch project.<date>-<first words>-<id tail>folder under the root, using feat: start threads without a project #13612's naming and its fallback from the short id to the full id.thread:….Schema.TaggedErrors,ScratchWorkspaceUnavailableError,ScratchWorkspaceFolderErrorandScratchWorkspaceProjectError, with bounded messages andcausekept.Where threads get their folder.
ThreadLaunchService.launchclaims a folder when a Scratch thread is launched with therootworkspace strategy, and binds it the way it binds an existing worktree (worktreePathon create, then preparation runs there). That covers:orchestration.launchThread);t3_thread_launch;A retried launch keeps the folder its first attempt bound. Threads in other projects, and non-root strategies, are untouched.
Two paths don't go through the launch service:
thread.createcommand, which web uses only for "implement plan in a new thread" (it copies the parent'sworktreePath);create_threads, which inherits the parent's folder.Both run in an existing folder, as in #13612.
ws.tsis now a few lines:projects.ensureScratchcallsensureProject;scratchWorkspaceRootfromroot.MCP.
t3_thread_launchtakesscratch: true, which resolves the project withensureProjectand then launches at the root, so the thread gets its own folder. Combining it withprojectIdorworkspaceStrategyis rejected.Layer. The service is provided in
runtimeLayer.tsnext toProjectService, forThreadLaunchService,ws.tsand the MCP handlers.Docs. The user docs #13612 added (
thread-sidebar.md,keybindings.md) still describe the behaviour. The MCP doc gains one sentence onscratch: true.Verification
tsc --noEmit -p apps/server: exit 0, with noerror TSorwarning TS.unshare -U -p(112 passed):ScratchWorkspace.test.ts(new): realProjectServiceon SQLite, real Git detection, real temp folders.git init'd one.ensureProjectcalls give one titled, iconed project.ensureProjectand forfolderForThread.ThreadLaunchService.test.ts: a new test checks that a root launch in the Scratch project binds its folder and runs setup there, that a retry claims no second folder, and that another project gets none.mcp/toolkits/project/handlers.test.ts: a new test checks thatscratch: truelaunches into the Scratch project and thatscratchplusprojectIdis refused.core.test.ts,ws.test.ts,ScheduledTaskService.test.tsandruntimeLayer.test.ts, andOrchestratorReplayFixtures.integration.test.tsbefore the rebase.vp linton the touched files: no new warnings (the existingws.tsand test warnings are on the base).vp fmt --checkis clean, andvp run knip:checkis clean.ns-audit.py) finds 0 violations in the touched files.Model: Claude Opus 5.5 (Claude Code)
🤖 Generated with Claude Code