Skip to content

perf: Faster task startup (reserve worktrees + direct CLI spawn)#674

Merged
rabanspiegel merged 36 commits intomainfrom
emdash/start-work-68k
Jan 24, 2026
Merged

perf: Faster task startup (reserve worktrees + direct CLI spawn)#674
rabanspiegel merged 36 commits intomainfrom
emdash/start-work-68k

Conversation

@rabanspiegel
Copy link
Contributor

@rabanspiegel rabanspiegel commented Jan 21, 2026

Summary

Task creation is now ~50% faster through two optimizations:

1. Reserve Worktree Pool

  • Pre-creates worktrees in background when project is opened
  • Claiming a reserve is instant (~80ms vs ~2300ms)
  • Fallback to sync creation if no reserve available
  • 30-minute staleness threshold
  • Startup cleanup for orphaned reserves

2. Direct CLI Spawn

  • Spawns CLI executables directly (bypasses shell config loading)
  • Saves ~800ms by skipping oh-my-zsh, nvm, pyenv, etc.
  • Shell spawns automatically after CLI exits (ctrl+c, /exit)

Benchmark Results

CleanShot 2026-01-21 at 18 10 41@2x

A couple more notes here: This is all vs Claude. Claude itself takes 1.0 - 1.5 seconds to spawn (on my machine). We should look into improving the terminal mount times. Also percentage improvement depends on how long the worktree creation would've taken synchronously

Testing Checklist

  • Create task with fresh reserve (should be instant)
  • Create task without reserve (fallback works)
  • Create multiple tasks rapidly (replenishment works)
  • Global shell terminal still works
  • CLI exit spawns shell (can run git commands after)
  • App startup cleans orphaned reserves

Note

Improves perceived and actual task startup time by removing synchronous worktree creation and shell startup overhead.

  • Reserve worktree pool: new WorktreePoolService to pre-create/claim worktrees; startup orphan cleanup; shutdown cleanup; IPC endpoints (worktree:ensureReserve|hasReserve|claimReserve|removeReserve) exposed via preload
  • Direct PTY spawn: new pty:startDirect path using cached provider CLI; minimal env passthrough; falls back to shell; auto-spawns a shell after CLI exit; safer WC destroy handling
  • Terminal behavior: TerminalSessionManager/TerminalPane/registry accept providerId; smarter snapshot vs resume logic; reuse detection; performance timing hooks
  • Renderer UX: optimistic task creation (single and multi-agent) with background worktree creation; loading state for multi-agent; reserve kickoff when opening a project; removal of “isCreating” button states; fewer noisy logs
  • Main bootstrap: PATH/setup tweaks; warm provider cache; reserve cleanup on startup and before quit

Written by Cursor Bugbot for commit f6b52bc. This will update automatically on new commits. Configure here.

- Add WorktreePoolService to pre-create reserve worktrees in background
- Reserves are claimed instantly (~100ms) instead of sync creation (3-7s)
- Background replenishment after each claim
- 5-minute freshness check discards stale reserves
- Cleanup reserves on app shutdown
- Fallback to sync creation when no reserve available
- Show task in UI immediately after worktree is ready
- Move database save, telemetry, and issue seeding to background
- Reduces perceived task creation time from ~230ms to ~120ms
- Scan common worktree directories on app startup (after 2s delay)
- Clean up reserves in parallel for fast cleanup
- Handles crashes/forced quits that leave orphaned reserves
…ation

- Add direct PTY spawn (bypasses shell config loading, saves ~1-2s)
- Add USE_RESERVE toggle in App.tsx for benchmarking
- Add USE_DIRECT_SPAWN toggle in TerminalSessionManager.ts
- Add benchmark logging for worktree and PTY timing
- Defer worktree shell spawn to not compete with CLI
- Clean up WorktreePoolService logging
- Remove warm PTY code (replaced by direct spawn)
- Remove USE_RESERVE and USE_DIRECT_SPAWN toggles
- Remove benchmark timing passthrough (taskCreateStartTime, worktreeTimeMs)
- Remove logFirstDataTiming() and benchmark box output
- Always use reserve worktree (with fallback to create)
- Always use direct spawn for provider CLIs (shell-based for terminals)

Benchmark results (7059ms → 3706ms, 47% faster):
- Reserve worktree: 2354ms → 85ms
- Direct CLI spawn: 3070ms → 1425ms
- Increase MAX_RESERVE_AGE_MS from 5 to 30 minutes
- When direct-spawned CLI exits (ctrl+c, /exit), spawn shell automatically
- User can continue working in terminal (git commands, etc.)
- Shell respawn notifies renderer via pty:shellSpawned event
- Don't delete owner on direct CLI exit (needed for shell respawn)
- Remove unnecessary 100ms delay
- Shell now spawns immediately and user can type
@vercel
Copy link

vercel bot commented Jan 21, 2026

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

Project Deployment Review Updated (UTC)
docs Ready Ready Preview, Comment Jan 24, 2026 9:22pm

Request Review

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@rabanspiegel rabanspiegel changed the title perf: 47% faster task startup (reserve worktrees + direct CLI spawn) perf: Faster task startup (reserve worktrees + direct CLI spawn) Jan 22, 2026
@rabanspiegel rabanspiegel marked this pull request as ready for review January 22, 2026 02:51
Resolve conflicts keeping:
- Direct CLI spawn (providerId prop) for faster startup
- Main's conversation/multi-chat improvements
- Main's disableSnapshots and forwardRef for TerminalPane
- Main's improved skipResume logic for Claude sessions
cursor[bot]

This comment was marked as outdated.

@cursor
Copy link

cursor bot commented Jan 22, 2026

Bugbot Autofix prepared a fix for all 6 of the 6 bugs found in the latest run.

  • ✅ Fixed: Async startPty called without await causes shell spawn failure
    • Made setOnDirectCliExit callback async and added await before startPty call to properly resolve the Promise.
  • ✅ Fixed: Silent database save failure loses task data
    • Added toast notification in the saveTask error handler to alert users when task persistence fails.
  • ✅ Fixed: Glob patterns not handled in preserveFiles method
    • Updated preserveFiles to use minimatch for glob pattern matching instead of treating patterns as literal filenames.
  • ✅ Fixed: Direct spawn missing resume flag breaks conversation continuity
    • Added resumeFlag handling to startDirectPty to enable conversation continuity for providers like Claude and Aider.
  • ✅ Fixed: Direct spawn missing WebContents destroyed cleanup causes resource leak
    • Added wc.once('destroyed') cleanup handler to pty:startDirect to properly kill PTY processes when window is closed.
  • ✅ Fixed: Base ref switch failure silently ignored during task creation
    • Added check for claimResult.needsBaseRefSwitch and display toast notification to warn users when base ref switch fails.

Create PR

- Add await to startPty in CLI exit handler (was returning Promise, not IPty)
- Add WebContents destroyed cleanup to pty:startDirect (prevent orphaned processes)
- Add resume flag support to startDirectPty (conversation continuity)
- Show toast warning on database save failure (user awareness)
- Warn user if base ref switch fails during reserve claim
- Handle glob patterns properly in preserveFiles (.env.*.local)
…spawn

Without this check, Claude would start with -c -r flag even for new tasks
where no session exists, causing it to exit immediately and trigger shell
spawn instead of starting the CLI.
cursor[bot]

This comment was marked as outdated.

@cursor
Copy link

cursor bot commented Jan 22, 2026

Bugbot Autofix prepared a fix for 2 of the 2 bugs found in the latest run.

  • ✅ Fixed: Shell spawn after CLI exit uses wrong terminal dimensions
    • Fixed by storing terminal dimensions in PTY record and using them when spawning shell, replacing hardcoded 120x32 values.
  • ✅ Fixed: Direct spawn missing skip-resume check for additional chats
    • Fixed by adding additional chat detection (id includes '-chat-' but not '-main-') and forcing skipResume=true in direct spawn path.

Create PR

Resolve conflicts with provider->agent rename while preserving direct CLI spawn optimization (providerId prop).
@cursor
Copy link

cursor bot commented Jan 22, 2026

Bugbot Autofix prepared a fix for all 6 of the 6 bugs found in the latest run.

  • ✅ Fixed: Reserve worktrees use stale refs without fetching
    • Added git fetch operation when claiming reserves to ensure users always work on up-to-date code from the remote.
  • ✅ Fixed: Shell spawn after CLI exit lacks renderer notification
    • Changed to send pty:started event (which renderer already listens for) and used safeSendToOwner to prevent crashes.
  • ✅ Fixed: Stale reserves not replaced proactively
    • Modified ensureReserve to check staleness and proactively replace stale reserves before users attempt to claim them.
  • ✅ Fixed: Orphaned reserves in custom paths not cleaned
    • Enhanced cleanupOrphanedReserves to scan database project paths in addition to hardcoded directories for comprehensive cleanup.
  • ✅ Fixed: Missing destroyed check before shell notification
    • Replaced direct wc.send with safeSendToOwner which checks isDestroyed() before sending IPC messages.
  • ✅ Fixed: Partial transform leaves orphaned worktree on failure
    • Added logic to detect and clean up moved worktrees by checking if original path exists and scanning for the moved location.

Create PR

Remove E2E timing console.log that was appearing in production.
Removes clickTime and worktreeMs tracking used only for benchmarks.
Dead code from optimization work - tracked spawn times but was never consumed by the renderer.
When ptyStartDirect fails because the CLI path isn't in the cache
(e.g., cache corruption, CLI uninstalled, race at startup), fall back
to shell-based spawn instead of leaving the terminal in a broken state.
setSelectedProject((prev) =>
prev ? { ...prev, tasks: prev.tasks?.filter((t) => t.id !== groupId) } : null
);
setActiveTask((current) => (current?.projectId === selectedProject.id ? null : current));
Copy link

Choose a reason for hiding this comment

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

Incorrect task ID check clears wrong active task

Medium Severity

The setActiveTask calls in the multi-agent background task creation use current?.projectId === selectedProject.id to check which task to update, but this incorrectly matches ANY task in the project rather than the specific optimistic task (groupId). Since worktree creation happens asynchronously, users can switch to a different task during this time. If an error occurs, removeOptimisticTask clears the wrong task. Similarly in the success path, the finalTask overwrites whatever task is currently active in the project rather than updating only the original optimistic task.

Additional Locations (1)

Fix in Cursor Fix in Web

autoApprove={autoApproveEnabled}
env={undefined}
keepAlive={true}
disableSnapshots={false}
Copy link

Choose a reason for hiding this comment

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

Terminal renders before conversation loads causing ID mismatch

Medium Severity

Removing the conversationsLoaded guard causes TerminalPane to render immediately with a fallback terminalId (${agent}-main-${task.id}). When conversations subsequently load and a non-main conversation is the active one, terminalId changes to ${agent}-chat-${conversationId}. This triggers a session switch: the old session detaches (but stays alive due to keepAlive={true}) while a new session spawns. This creates orphaned PTY processes and causes visible terminal flicker for users with non-main conversations active.

Fix in Cursor Fix in Web

1. Check current?.id === groupId instead of matching any task in the
   project by projectId (affects removeOptimisticTask and success path)
2. Restore conversationsLoaded guard for TerminalPane to prevent terminal
   ID mismatch when conversations load after initial render
1. Use safeSendToOwner() in direct spawn and shell respawn handlers
   to prevent crashes when WebContents is destroyed during data send
2. Clean up reserve worktree when deleting a project to prevent
   orphaned worktrees accumulating on disk
maybeMarkProviderFinish(id, exitCode, signal);
// DON'T delete owner/listeners - shell will be spawned and reuse them
listeners.delete(id);
});
Copy link

Choose a reason for hiding this comment

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

Memory leak in direct PTY fallback exit handler

Medium Severity

The pty:startDirect exit handler intentionally skips calling owners.delete(id) because for direct CLI spawns, a shell is spawned afterward that reuses the owner. However, when the direct spawn falls back to startPty (lines 402-418), no subsequent shell is spawned because the PTY record lacks isDirectSpawn: true. The fallback shell exits normally but owners is never cleaned up, causing WebContents references to accumulate until the window is destroyed.

Fix in Cursor Fix in Web

disableSnapshots={
!conversations.find((c) => c.id === activeConversationId)?.isMain
}
disableSnapshots={false}
Copy link

Choose a reason for hiding this comment

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

Snapshot setting change enables snapshots for all conversations

Medium Severity

The disableSnapshots prop was changed from conditionally checking !conversations.find((c) => c.id === activeConversationId)?.isMain to a hardcoded false. This means terminal snapshots are now saved for all conversations, including non-main chats, which was previously disabled by design. This behavioral change appears unintentional given the PR is focused on performance optimizations, not snapshot behavior.

Fix in Cursor Fix in Web

1. Wrap applySnapshot in try/catch to prevent unhandled rejections
2. Fix misleading comment about owner/listeners cleanup in exit handler
Copy link

@cursor cursor bot left a comment

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 and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

@rabanspiegel
Copy link
Contributor Author

lgtm :)

@rabanspiegel rabanspiegel merged commit 75396b1 into main Jan 24, 2026
4 checks passed
@rabanspiegel rabanspiegel deleted the emdash/start-work-68k branch January 24, 2026 22:30
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