Skip to content

fix(host-service): classify missing-worktree git errors as NOT_FOUND instead of 500 - #6199

Merged
saddlepaddle merged 1 commit into
mainfrom
sentry/git-missing-worktree
Aug 5, 2026
Merged

saddlepaddle merged 1 commit into
mainfrom
sentry/git-missing-worktree

Conversation

@saddlepaddle

@saddlepaddle saddlepaddle commented Aug 5, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

HOST-SERVICE-W (escalating, 16 events/minute on canary 1.18.3-canary.20260805123527): when a workspace's worktree directory no longer exists — deleted externally or a cleanup raced the poll loop — every polled git tRPC endpoint (git.getStatus, git.listCommits, git.listBranches, git.getBranchSyncStatus, ...) constructs simple-git on the missing path, throws GitConstructError: Cannot use simple-git on a directory that does not exist, and escapes as INTERNAL_SERVER_ERROR into Sentry — once per batched procedure per poll.

This is an expected lifecycle state, not a bug, so per the error-handling contract (#6164) it must be classified at the throw site — the dumb 500=report Sentry middleware stays filter-free.

Fix

  • resolveWorktreePath — the shared boundary every git.* procedure resolves the workspace path through — now checks the directory exists and throws a typed NOT_FOUND with cause { kind: "WORKTREE_MISSING", worktreePath }, mirroring the desktop v1 changes-router convention. Non-500 → not captured by the Sentry middleware; the renderer already degrades on error results (workspace.get already exposes worktreeExists).
  • workspace.gitStatus (legacy CLI/SDK surface) had the same hole via a direct ctx.git(worktreePath); it now routes through the same helper.

Related

DESKTOP-H0 is the same error via the desktop project, but through the desktop v1 changes/workspaces routers. That path is already classified on main (pathExistsCached guard → NOT_FOUND WORKTREE_MISSING, plus GitEnvironmentError → PRECONDITION_FAILED); its remaining events all come from released builds ≤ 1.18.3 whose bundles predate those guards (verified against event stacks), so it drains as users update — no further code change needed.

Refs HOST-SERVICE-W

https://claude.ai/code/session_01JAJWZrVSdXGbsfiGvwCyKW


Summary by cubic

Classified missing workspace worktrees as NOT_FOUND to prevent simple-git construct errors from surfacing as 500s during git polling. This stops Sentry noise (Refs HOST-SERVICE-W) and keeps the UI degrading gracefully.

  • Bug Fixes
    • Added an existence check in resolveWorktreePath to throw NOT_FOUND with { kind: "WORKTREE_MISSING", worktreePath }, preventing simple-git construct errors from escaping as 500s across all git.* procedures.
    • Routed workspace.gitStatus through resolveWorktreePath to apply the same guard.

Written for commit 9743185. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling when a workspace worktree is missing.
    • Git status requests now return a clear “not found” error instead of failing unexpectedly.
    • Added structured error details to help identify missing worktree paths.

…instead of 500

When a workspace's worktree directory is deleted externally (or cleanup
races a poll), every git.* procedure hit simple-git's GitConstructError
("Cannot use simple-git on a directory that does not exist") and escaped
as INTERNAL_SERVER_ERROR into Sentry once per batched procedure per poll.

Guard the shared resolveWorktreePath boundary with an existence check and
throw a typed NOT_FOUND with cause { kind: "WORKTREE_MISSING" }, matching
the desktop v1 changes-router convention. Also route the legacy
workspace.gitStatus surface through the same helper.

Fixes HOST-SERVICE-W

Claude-Session: https://claude.ai/code/session_01JAJWZrVSdXGbsfiGvwCyKW
@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The worktree resolver now verifies that the path exists on disk. gitStatus uses this resolver before creating the Git client, returning structured NOT_FOUND errors for missing worktrees.

Changes

Worktree validation

Layer / File(s) Summary
Validate resolved worktree paths
packages/host-service/src/trpc/router/git/utils/resolve-worktree.ts
The resolver checks worktree existence and raises a NOT_FOUND TRPCError with WORKTREE_MISSING cause metadata when the path is missing.
Use resolver in gitStatus
packages/host-service/src/trpc/router/workspace/workspace.ts
gitStatus resolves the worktree path before creating the Git client and removes its direct workspace lookup and local missing-workspace handling.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: kitenite

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title uses conventional commit format and clearly describes the missing-worktree error classification change.
Description check ✅ Passed The description clearly explains the problem and fix, but it does not include the template's testing or checklist sections.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sentry/git-missing-worktree

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR classifies absent host-service worktrees as NOT_FOUND and routes the legacy workspace git-status endpoint through the shared resolver.

  • Adds a filesystem existence guard and structured WORKTREE_MISSING cause.
  • Reuses the shared worktree resolver in workspace.gitStatus.
  • The guard still leaves a check-to-use race before the actual git command.

Confidence Score: 4/5

The remaining cleanup race should be fixed before merging because a worktree can still disappear after the new guard and produce the same reportable server error.

The existence check and the git command are not atomic or synchronized, so concurrent cleanup can still bypass the new NOT_FOUND classification.

Files Needing Attention: packages/host-service/src/trpc/router/git/utils/resolve-worktree.ts

Important Files Changed

Filename Overview
packages/host-service/src/trpc/router/git/utils/resolve-worktree.ts Adds the missing-worktree classification, but the standalone existence check does not cover deletion between resolution and the git operation.
packages/host-service/src/trpc/router/workspace/workspace.ts Routes legacy git status through the shared resolver, inheriting both its intended classification and its remaining race window.
Prompt To Fix All With AI
### Issue 1
packages/host-service/src/trpc/router/git/utils/resolve-worktree.ts:23
**Worktree check leaves race window**

If workspace cleanup or external deletion occurs after `existsSync` succeeds but before the git command starts, the unprotected path reaches simple-git and the missing directory still escapes as an internal server error, producing the repeated Sentry events this guard is intended to prevent.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(host-service): classify missing-work..." | Re-trigger Greptile

// A worktree deleted outside the app is a routine lifecycle state, not a
// bug — classify it here so simple-git's construct error can't escape any
// git.* procedure as a reportable 500.
if (!existsSync(workspace.worktreePath)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Worktree check leaves race window

If workspace cleanup or external deletion occurs after existsSync succeeds but before the git command starts, the unprotected path reaches simple-git and the missing directory still escapes as an internal server error, producing the repeated Sentry events this guard is intended to prevent.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/host-service/src/trpc/router/git/utils/resolve-worktree.ts
Line: 23

Comment:
**Worktree check leaves race window**

If workspace cleanup or external deletion occurs after `existsSync` succeeds but before the git command starts, the unprotected path reaches simple-git and the missing directory still escapes as an internal server error, producing the repeated Sentry events this guard is intended to prevent.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/host-service/src/trpc/router/workspace/workspace.ts`:
- Around line 134-135: Update the workspace flow around resolveWorktreePath and
ctx.git so missing-worktree errors raised by git.status(), including the removal
race after path resolution, are translated to the expected missing-worktree
response instead of INTERNAL_SERVER_ERROR. Handle only this missing-worktree
case, preferably in the shared ctx.git factory if appropriate, and add a
regression test covering removal between resolution and status.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bf28fe0e-75bb-4434-bd61-1f328e83a3f0

📥 Commits

Reviewing files that changed from the base of the PR and between 21db9f5 and 9743185.

📒 Files selected for processing (2)
  • packages/host-service/src/trpc/router/git/utils/resolve-worktree.ts
  • packages/host-service/src/trpc/router/workspace/workspace.ts

Comment on lines +134 to +135
const worktreePath = resolveWorktreePath(ctx, input.id);
const git = await ctx.git(worktreePath);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 8 \
  'ctx\.git|simple-git|git\.status\(\)|WORKTREE_MISSING|INTERNAL_SERVER_ERROR' \
  packages/host-service/src

Repository: superset-sh/superset

Length of output: 50377


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== workspace gitStatus =="
sed -n '100,170p' packages/host-service/src/trpc/router/workspace/workspace.ts

echo "== candidate git simple-file factories =="
fd -a 'simple-git|git-env|git' packages/host-service/src/runtime packages/host-service/src/db packages/host-service/src/trpc | sed 's#^\./##' | head -80

echo "== locate create git factory definitions/usages =="
rg -n -C 5 'createUserSimpleGit|ctx\.git\(|git\(:Promise<|git:|GitEnvResolver|GitTaskEnv' packages/host-service/src/runtime packages/host-service/src/trpc packages/host-service/src/db --glob '*.ts' | head -300

echo "== error types and constants =="
rg -n -C 5 'NOT_FOUND|PRECONDITION_FAILED|INTERNAL_SERVER_ERROR|ERR_*|ENOENT|ENOTDIR' packages/host-service/src/trpc packages/host-service/src/runtime packages/host-service/src/runtime/git* --glob '*.ts' | head -300

Repository: superset-sh/superset

Length of output: 50376


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
# Behavioral probe: Node fs.stat existsSync -> stat operation exists while other process deletes target.
# Shows why Line 134 preflight does not eliminate the race before ctx.git(worktreePath) / git.status().
import os
import tempfile
path = tempfile.mkdtemp()
os.rmdir(path)
exists_before = os.path.exists(path)
try:
    fd = os.open(path, os.O_RDONLY|os.O_NONBLOCK)
except FileNotFoundError as e:
    exists_after = False
    error_type = type(e).__name__
else:
    os.close(fd)
    exists_after = os.path.exists(path)
    error_type = "no error in this path"
print({"path": path, "exists_before": exists_before, "exists_after": exists_after, "error_type": error_type})
PY

Repository: superset-sh/superset

Length of output: 269


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== git factory and runtime config =="
sed -n '1,90p' packages/host-service/src/runtime/git/git.ts
rg -n -C 4 'createGitFactory|ctx\.git\s*=|gitFactory|HostServiceContext' packages/host-service/src --glob '*.ts'

echo "== workspace path resolver =="
rg -n -C 8 'function resolveWorktreePath|const resolveWorktreePath|resolveWorktreePath' packages/host-service/src --glob '*.ts'

echo "== git test coverage around missing repo =="
sed -n '1,140p' packages/host-service/src/runtime/git/git.test.ts
sed -n '1,130p' packages/host-service/src/runtime/git/simple-git.test.ts
sed -n '1,180p' packages/host-service/src/trpc/router/git/utils/git-status.integration.test.ts

Repository: superset-sh/superset

Length of output: 50377


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
# Behavioral probe: simple-git's status operation is spawned after client construction and can
# observe ENOENT if the directory is removed between constructor and status execution.
print("Node ENOENT can occur on filesystem operations scheduled after the path exists")
print("createUserSimpleGit calls simpleGit(baseDir), but git.status() is the later filesystem operation")
PY

Repository: superset-sh/superset

Length of output: 334


Translate missing-worktree errors from git.status().

gitStatus can still raise INTERNAL_SERVER_ERROR when another process removes worktreePath after resolveWorktreePath and before or during git.status(). Translate only the missing-worktree case here or in the shared ctx.git factory, and add a race regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/host-service/src/trpc/router/workspace/workspace.ts` around lines
134 - 135, Update the workspace flow around resolveWorktreePath and ctx.git so
missing-worktree errors raised by git.status(), including the removal race after
path resolution, are translated to the expected missing-worktree response
instead of INTERNAL_SERVER_ERROR. Handle only this missing-worktree case,
preferably in the shared ctx.git factory if appropriate, and add a regression
test covering removal between resolution and status.

Source: MCP tools

@github-actions

github-actions Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/host-service/src/trpc/router/git/utils/resolve-worktree.ts">

<violation number="1" location="packages/host-service/src/trpc/router/git/utils/resolve-worktree.ts:23">
P2: The existsSync guard classifies the missing-worktree case only when the directory is already gone at check time. Because simple-git is constructed afterwards (ctx.git(worktreePath) / createUserSimpleGit), a worktree deleted in the tiny window between this check and the git construct still throws GitConstructError and escapes as a reportable 500 — the exact symptom (HOST-SERVICE-W) this PR is meant to stop. The added comment's claim that the construct error "can't escape any git.* procedure" is stronger than what the check delivers. Consider classifying at the git-construct boundary instead (wrap the simple-git construction so GitConstructError on a missing worktree becomes NOT_FOUND/WORKTREE_MISSING), which removes the race window entirely.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Poller as Git Poll Loop
    participant TRPC as tRPC Router (git.*)
    participant Resolve as resolveWorktreePath
    participant DB as SQLite Database
    participant FS as Filesystem
    participant SimpleGit as simple-git
    participant Sentry as Sentry Middleware
    participant Renderer as React Renderer

    Note over Poller,Renderer: Happy Path — Worktree exists on disk

    Poller->>TRPC: poll git.getStatus / git.listCommits / etc.
    TRPC->>Resolve: resolveWorktreePath(ctx, workspaceId)
    Resolve->>DB: query workspaces WHERE id = workspaceId
    DB-->>Resolve: workspace record (with worktreePath)
    Resolve->>FS: existsSync(worktreePath)
    FS-->>Resolve: true
    Resolve-->>TRPC: worktreePath
    TRPC->>SimpleGit: construct simple-git on worktreePath
    SimpleGit-->>TRPC: git command result
    TRPC-->>Poller: success response
    Poller-->>Renderer: workspace data (worktreeExists: true)

    Note over Poller,Renderer: NEW: Happy Path — Worktree missing on disk

    Poller->>TRPC: poll git.* procedure
    TRPC->>Resolve: resolveWorktreePath(ctx, workspaceId)
    Resolve->>DB: query workspace record
    DB-->>Resolve: workspace record (stale worktreePath)
    Resolve->>FS: existsSync(worktreePath)
    FS-->>Resolve: false
    Resolve->>Resolve: throw TRPCError(NOT_FOUND, { kind: "WORKTREE_MISSING", worktreePath })
    Resolve-->>TRPC: exception escapes
    alt NOT_FOUND code (non-500)
        TRPC->>Sentry: NOT_FOUND → not captured
        TRPC-->>Poller: TRPCError with code: "NOT_FOUND"
        Poller-->>Renderer: error result (worktreeExists check degraded)
    else any other error code (500)
        TRPC->>Sentry: captured and reported
        TRPC-->>Poller: INTERNAL_SERVER_ERROR
    end

    Note over Resolve,SimpleGit: Previously: missing worktree would reach simple-git construct → GitConstructError → 500

    Note over TRPC,Resolve: workspace.gitStatus procedure also routes through resolveWorktreePath

    Poller->>TRPC: workspace.gitStatus({ id })
    TRPC->>Resolve: resolveWorktreePath(ctx, id) (CHANGED: was inline db query + ctx.git)
    Resolve->>DB: query workspace
    DB-->>Resolve: workspace record
    alt worktree exists
        Resolve->>FS: existsSync → true
        Resolve-->>TRPC: worktreePath
        TRPC->>SimpleGit: ctx.git(worktreePath)
        SimpleGit-->>TRPC: git status result
        TRPC-->>Poller: status data
    else worktree missing
        Resolve->>FS: existsSync → false
        Resolve->>Resolve: throw NOT_FOUND
        Resolve-->>TRPC: exception
        TRPC-->>Poller: NOT_FOUND error
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

// A worktree deleted outside the app is a routine lifecycle state, not a
// bug — classify it here so simple-git's construct error can't escape any
// git.* procedure as a reportable 500.
if (!existsSync(workspace.worktreePath)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The existsSync guard classifies the missing-worktree case only when the directory is already gone at check time. Because simple-git is constructed afterwards (ctx.git(worktreePath) / createUserSimpleGit), a worktree deleted in the tiny window between this check and the git construct still throws GitConstructError and escapes as a reportable 500 — the exact symptom (HOST-SERVICE-W) this PR is meant to stop. The added comment's claim that the construct error "can't escape any git.* procedure" is stronger than what the check delivers. Consider classifying at the git-construct boundary instead (wrap the simple-git construction so GitConstructError on a missing worktree becomes NOT_FOUND/WORKTREE_MISSING), which removes the race window entirely.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/trpc/router/git/utils/resolve-worktree.ts, line 23:

<comment>The existsSync guard classifies the missing-worktree case only when the directory is already gone at check time. Because simple-git is constructed afterwards (ctx.git(worktreePath) / createUserSimpleGit), a worktree deleted in the tiny window between this check and the git construct still throws GitConstructError and escapes as a reportable 500 — the exact symptom (HOST-SERVICE-W) this PR is meant to stop. The added comment's claim that the construct error "can't escape any git.* procedure" is stronger than what the check delivers. Consider classifying at the git-construct boundary instead (wrap the simple-git construction so GitConstructError on a missing worktree becomes NOT_FOUND/WORKTREE_MISSING), which removes the race window entirely.</comment>

<file context>
@@ -16,5 +17,15 @@ export function resolveWorktreePath(
+	// A worktree deleted outside the app is a routine lifecycle state, not a
+	// bug — classify it here so simple-git's construct error can't escape any
+	// git.* procedure as a reportable 500.
+	if (!existsSync(workspace.worktreePath)) {
+		throw new TRPCError({
+			code: "NOT_FOUND",
</file context>

This branch was previously deployed

1 inactive deployment
preview — 97431855 Deployed Aug 5, 2026 by saddlepaddle via Deploy Marketing #7139
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