Skip to content

refactor(server): threads without a project run through a ScratchWorkspace service - #14521

Merged
juliusmarminge merged 17 commits into
t3code/codex-turn-mappingfrom
v2/scratch-workspace-service
Oct 1, 2026
Merged

juliusmarminge merged 17 commits into
t3code/codex-turn-mappingfrom
v2/scratch-workspace-service

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

#13612 started threads without a project from inline code in the ws.ts WebSocket 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 a ScratchWorkspace service, 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, a Context.Service with make/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 on ProjectService.bootstrap, with the dashed-bubble icon set only at create.
    • bootstrap looks up the root before it takes the workspace lock, so concurrent first calls do race. The loser's ProjectConflictError names the winner, and the service returns that project.
    • The folder is re-created on every call.
  • folderForThread({ projectId, threadId, text }): Some only for the Scratch project.
    • It claims a fresh <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.
    • The short name uses the id's tail, because V2 thread ids share prefixes such as thread:….
  • Errors are Schema.TaggedErrors, ScratchWorkspaceUnavailableError, ScratchWorkspaceFolderError and ScratchWorkspaceProjectError, with bounded messages and cause kept.

Where threads get their folder. ThreadLaunchService.launch claims a folder when a Scratch thread is launched with the root workspace strategy, and binds it the way it binds an existing worktree (worktreePath on create, then preparation runs there). That covers:

  • web and mobile first turns (orchestration.launchThread);
  • MCP t3_thread_launch;
  • scheduled tasks;
  • ACP session import;
  • startup auto-bootstrap.

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:

  • the bare thread.create command, which web uses only for "implement plan in a new thread" (it copies the parent's worktreePath);
  • MCP create_threads, which inherits the parent's folder.

Both run in an existing folder, as in #13612.

ws.ts is now a few lines:

  • projects.ensureScratch calls ensureProject;
  • the config answer adds scratchWorkspaceRoot from root.

MCP. t3_thread_launch takes scratch: true, which resolves the project with ensureProject and then launches at the root, so the thread gets its own folder. Combining it with projectId or workspaceStrategy is rejected.

Layer. The service is provided in runtimeLayer.ts next to ProjectService, for ThreadLaunchService, ws.ts and the MCP handlers.

Docs. The user docs #13612 added (thread-sidebar.md, keybindings.md) still describe the behaviour. The MCP doc gains one sentence on scratch: true.

Verification

  • tsc --noEmit -p apps/server: exit 0, with no error TS or warning TS.
  • Inside unshare -U -p (112 passed):
    • ScratchWorkspace.test.ts (new): real ProjectService on SQLite, real Git detection, real temp folders.
      • Scratch is offered outside a checkout and refused inside a git init'd one.
      • Eight concurrent ensureProject calls give one titled, iconed project.
      • The folder is re-created after deletion, both for ensureProject and for folderForThread.
      • Two threads with the same message get distinct folders named from it.
      • A pasted blob stays one bounded path segment.
      • A thread in another project gets no folder.
    • 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 that scratch: true launches into the Scratch project and that scratch plus projectId is refused.
    • Also core.test.ts, ws.test.ts, ScheduledTaskService.test.ts and runtimeLayer.test.ts, and OrchestratorReplayFixtures.integration.test.ts before the rebase.
  • Mutation checks:
    • dropping the conflict handling fails the race test;
    • always offering Scratch fails the checkout test;
    • not passing the folder to preparation fails the launch test.
  • vp lint on the touched files: no new warnings (the existing ws.ts and test warnings are on the base). vp fmt --check is clean, and vp run knip:check is clean.
  • The namespace audit (ns-audit.py) finds 0 violations in the touched files.
  • Not run: a live client pass, and the full suites.

Model: Claude Opus 5.5 (Claude Code)

🤖 Generated with Claude Code


Devin Review

shivamhwp and others added 15 commits September 30, 2026 07:05
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>
Brings in f48d257 (#14502), which only touches CheckpointRestoreSafety.ts
and its test. No conflicts.

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>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Oct 1, 2026
@juliusmarminge juliusmarminge added the macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews label Oct 1, 2026
Comment thread apps/server/src/project/ScratchWorkspace.ts Outdated
Comment thread apps/server/src/project/ScratchWorkspace.ts
Comment thread apps/server/src/project/ScratchWorkspace.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

Comment thread apps/server/src/project/ScratchWorkspace.ts
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ No successful main baseline artifact is available yet. This run establishes the initial measurement.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire — 4.9 KiB — 6.8 KiB ✅
Codex Thread snapshot wire — 3.7 KiB — 4.9 KiB ✅
Codex Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Codex Live turn WebSocket decoded — 20.4 KiB — 29.3 KiB ✅
Codex Live turn messages — 2 — 8 ✅
Claude Total thread wire — 4.9 KiB — 6.8 KiB ✅
Claude Thread snapshot wire — 3.7 KiB — 4.9 KiB ✅
Claude Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Claude Live turn WebSocket decoded — 20.8 KiB — 29.3 KiB ✅
Claude Live turn messages — 2 — 8 ✅

Baseline: unavailable · PR result: d17305f · Source CI: success

Scenario and decoded snapshot size

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

  • Codex decoded thread snapshot: 106.1 KiB
  • Claude decoded thread snapshot: 106.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

juliusmarminge and others added 2 commits September 30, 2026 19:53
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>
@juliusmarminge
juliusmarminge changed the base branch from v2/main-sync-0930 to t3code/codex-turn-mapping October 1, 2026 03:05
@juliusmarminge
juliusmarminge merged commit d17305f into t3code/codex-turn-mapping Oct 1, 2026
24 checks passed
@juliusmarminge
juliusmarminge deleted the v2/scratch-workspace-service branch October 1, 2026 03:05
Comment thread apps/mobile/src/features/threads/NewTaskRouteScreen.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants