Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion internal/app/daemon/app_headless.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"log"
"os"
"strings"
"time"

Expand Down Expand Up @@ -189,7 +190,12 @@ func (a *App) startManagedHeadless(command control.DaemonCommand) []eventcontrac
firstNonEmpty(command.WorkspaceKey, command.ThreadCWD),
err,
)
return a.handleManagedHeadlessLaunchFailure(command, err, now)
// When the launch fails because the workspace directory no longer
// exists, the OS reports an opaque "directory name is invalid" error.
// Replace it with an explanatory message so the Feishu client learns
// the real cause instead of a generic failure.
launchErr := explainMissingWorkspaceLaunchError(command, workDir, err)
return a.handleManagedHeadlessLaunchFailure(command, launchErr, now)
}

a.managedHeadlessRuntime.Processes[command.InstanceID] = &headlessruntime.Process{
Expand All @@ -214,6 +220,30 @@ func (a *App) startManagedHeadless(command control.DaemonCommand) []eventcontrac
return a.service.HandleHeadlessLaunchStarted(command.SurfaceSessionID, command.InstanceID, pid)
}

// explainMissingWorkspaceLaunchError upgrades an opaque launch failure into an
// explanatory one when the cause is that the workspace directory no longer
// exists on disk (e.g. it was moved or deleted after the thread was created).
// On any other failure it returns the original error untouched.
func explainMissingWorkspaceLaunchError(command control.DaemonCommand, workDir string, err error) error {
workDir = strings.TrimSpace(workDir)
if workDir == "" {
return err
}
if info, statErr := os.Stat(workDir); statErr == nil && info.IsDir() {
return err
}
return agentproto.ErrorInfo{
Code: "headless_workspace_dir_missing",
Layer: "daemon",
Stage: "headless_start",
Operation: "start_headless",
Message: fmt.Sprintf("工作区目录已不存在:%s,可能已被移动或删除。请重新选择目录,或重新接入该工作区。", workDir),
Details: err.Error(),
SurfaceSessionID: command.SurfaceSessionID,
ThreadID: command.ThreadID,
}
}

func (a *App) handleManagedHeadlessLaunchFailure(command control.DaemonCommand, err error, now time.Time) []eventcontract.Event {
events := a.service.HandleHeadlessLaunchFailed(command.SurfaceSessionID, command.InstanceID, err)
if !command.AutoRestore {
Expand Down
57 changes: 57 additions & 0 deletions internal/app/daemon/app_headless_workspace_missing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package daemon

import (
"errors"
"path/filepath"
"strings"
"testing"

"github.com/kxn/codex-remote-feishu/internal/core/agentproto"
"github.com/kxn/codex-remote-feishu/internal/core/control"
)

func TestExplainMissingWorkspaceLaunchErrorReplacesOpaqueError(t *testing.T) {
t.Parallel()

missingDir := filepath.Join(t.TempDir(), "deleted-workspace")
rawErr := errors.New(`fork/exec D:\Research\codex-remote-feishu\codex-remote.exe: The directory name is invalid.`)

got := explainMissingWorkspaceLaunchError(control.DaemonCommand{
SurfaceSessionID: "surface-1",
ThreadID: "thread-1",
}, missingDir, rawErr)

var info agentproto.ErrorInfo
if !errors.As(got, &info) {
t.Fatalf("expected ErrorInfo, got %T: %v", got, got)
}
if info.Code != "headless_workspace_dir_missing" {
t.Fatalf("unexpected code: %q", info.Code)
}
if !strings.Contains(info.Message, missingDir) || !strings.Contains(info.Message, "工作区目录已不存在") {
t.Fatalf("message does not explain the missing directory: %q", info.Message)
}
if !strings.Contains(info.Details, "directory name is invalid") {
t.Fatalf("expected raw error preserved in details, got %q", info.Details)
}
}

func TestExplainMissingWorkspaceLaunchErrorKeepsErrorWhenDirExists(t *testing.T) {
t.Parallel()

existingDir := t.TempDir()
rawErr := errors.New("some other launch failure")

got := explainMissingWorkspaceLaunchError(control.DaemonCommand{
SurfaceSessionID: "surface-1",
}, existingDir, rawErr)

if got != rawErr {
t.Fatalf("expected original error to be preserved when workspace exists, got %v", got)
}

// An empty workdir is not a missing-directory case either.
if got := explainMissingWorkspaceLaunchError(control.DaemonCommand{}, "", rawErr); got != rawErr {
t.Fatalf("expected original error for empty workDir, got %v", got)
}
}