feat: improve spawn error handling and reporting across full stack (#249)

* feat: improve spawn error handling and reporting across full stack

- Return error result instead of throwing in apiMachine spawn handler
- Add lastSpawnError field to RunnerState for persistent error tracking
- Add error awaiter system for early process exit/error detection before webhook
- Build detailed webhook failure messages with exit code, signal, and stderr tail
- Report spawn outcomes to hub via runner state updates
- Handle more spawn result types in rpcGateway with better error messages
- Display runner last spawn error in web UI (NewSession & SpawnSession)
- Extract shared formatRunnerSpawnError utility to avoid duplication

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): narrow spawnResult type check to fix TS2339 error

Use `type === 'error'` instead of `type !== 'success'` to properly
narrow the discriminated union, allowing TypeScript to infer errorMessage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
fireblue
2026-03-09 20:55:50 +08:00
committed by GitHub
co-authored by Claude Opus 4.6
parent b4b73d4405
commit 4716d315b7
8 changed files with 234 additions and 7 deletions
+15
View File
@@ -21,6 +21,7 @@ import {
} from './preferences'
import { SessionTypeSelector } from './SessionTypeSelector'
import { YoloToggle } from './YoloToggle'
import { formatRunnerSpawnError } from '../../utils/formatRunnerSpawnError'
export function NewSession(props: {
api: ApiClient
@@ -82,6 +83,15 @@ export function NewSession(props: {
}
}, [props.machines, machineId, getLastUsedMachineId, getRecentPaths])
const selectedMachine = useMemo(
() => (machineId ? props.machines.find((machine) => machine.id === machineId) ?? null : null),
[machineId, props.machines]
)
const runnerSpawnError = useMemo(
() => formatRunnerSpawnError(selectedMachine),
[selectedMachine]
)
const recentPaths = useMemo(
() => getRecentPaths(machineId),
[getRecentPaths, machineId]
@@ -247,6 +257,11 @@ export function NewSession(props: {
isDisabled={isFormDisabled}
onChange={handleMachineChange}
/>
{runnerSpawnError ? (
<div className="px-3 py-2 text-xs text-red-600">
Runner last spawn error: {runnerSpawnError}
</div>
) : null}
<DirectorySection
directory={directory}
suggestions={suggestions}
+11
View File
@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { usePlatform } from '@/hooks/usePlatform'
import { useSpawnSession } from '@/hooks/mutations/useSpawnSession'
import { formatRunnerSpawnError } from '@/utils/formatRunnerSpawnError'
import { useTranslation } from '@/lib/use-translation'
type SessionType = 'simple' | 'worktree'
@@ -32,6 +33,10 @@ export function SpawnSession(props: {
const { spawnSession, isPending, error: spawnError } = useSpawnSession(props.api)
const machineTitle = useMemo(() => getMachineTitle(props.machine), [props.machine])
const runnerSpawnError = useMemo(
() => formatRunnerSpawnError(props.machine),
[props.machine?.runnerState?.lastSpawnError]
)
async function spawn() {
const trimmed = directory.trim()
@@ -144,6 +149,12 @@ export function SpawnSession(props: {
</div>
</div>
{runnerSpawnError ? (
<div className="text-xs text-red-600">
Runner last spawn error: {runnerSpawnError}
</div>
) : null}
{(error ?? spawnError) ? (
<div className="text-sm text-red-600">
{error ?? spawnError}