Files
orca/src/main/runtime/settle-before-deadline.ts
T
OrcaWin 525ffc5ae0 fix(worktree): stop the PTY gate from permanently wedging workspace removal (#12153)
Destructive worktree removal proves every PTY is dead before touching the filesystem. When a stop
RPC failed, it re-listed the provider to check whether the PTY had already exited — but on the
same deadline the sweeps had just spent, so it timed out without ever asking and read "could not
verify" as "still live". The sweep spends that budget every run, making the refusal deterministic;
--force never reached the gate, so the workspace was unremovable forever.

- Verification gets its own budget instead of an exhausted remainder.
- Verdicts split into exited / live / unverifiable; the error names the blocking PTY ids and why.
- A reachable escape hatch: allowUnverifiedPtyStop, set only by genuine Force Delete affordances
  and the CLI's --force — never by the force the ordinary delete confirmation already sets — with
  an 'unstopped-pty' classifier reason so the desktop actually offers the button.
- Force also survives a sweep that cannot complete; the non-force path still fails fast.

Fixes #11960
2026-08-02 19:16:58 -07:00

55 lines
1.6 KiB
TypeScript

/**
* Races `run()` against an absolute deadline (epoch ms).
*
* Without `failClosedError` the call is best-effort: a timeout or a rejection
* resolves to `fallback`. With it, both surface as a rejection so destructive
* callers can block on unproven work — `failClosedOnRunError` narrows which
* rejections count (some are benign sentinels).
*/
export async function settleBeforeDeadline<T>(
run: () => Promise<T>,
fallback: T,
deadline: number,
failClosedError?: Error,
failClosedOnRunError: (error: unknown) => boolean = () => true
): Promise<T> {
const remaining = deadline - Date.now()
if (remaining <= 0) {
if (failClosedError) {
throw failClosedError
}
return fallback
}
return new Promise((resolve, reject) => {
let settled = false
const finish = (value: T): void => {
if (settled) {
return
}
settled = true
clearTimeout(timer)
resolve(value)
}
const fail = (error: unknown): void => {
if (settled) {
return
}
settled = true
clearTimeout(timer)
reject(error)
}
const timer = setTimeout(
() => (failClosedError ? fail(failClosedError) : finish(fallback)),
remaining
)
timer.unref?.()
// Why: `.then(run)` rather than `run()` so a synchronous throw is routed
// through the same fail-closed filter instead of escaping the executor.
void Promise.resolve()
.then(run)
.then(finish, (error: unknown) =>
failClosedError && failClosedOnRunError(error) ? fail(error) : finish(fallback)
)
})
}