mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 00:03:55 +00:00
* fix(worktrees): prevent deletion from blocking Orca * test(worktrees): loosen async history-delete event-loop bound for CI The main-thread safety check failed on a loaded runner when a single timer gap hit ~48ms under the prior 30ms threshold. Keep the bound well below a recursive sync-rm stall without treating CI jitter as a block. * test(worktrees): measure history-delete critical path, not timer gaps setInterval gaps during async rm of thousands of files still flake under CI scheduling. deleteWorktreeHistoryDir is sync and must only rename, so assert that critical-path wall time stays well below a recursive walk. * fix(worktrees): prevent deletion from blocking Orca Add timeout-based draining of watcher closes so SSH round-trip delays don't indefinitely block the worktree removal path. Also: order durable temp-file sweeps ahead of writes to reclaim orphans before accumulation, skip own-process temps to avoid deleting live writes, swallow persistence errors so disk failures don't cascade to query callers, and measure history-deletion progress by loop turns rather than timer gaps to detect blocking on CI runners. * fix(worktrees): prevent deletion from blocking Orca Worktree deletion can now proceed even if filesystem watchers or history cleanup operations hang, preventing Orca from freezing. Changes: - Fence install slots with tokens instead of counters so removals can abandon wedged installs without corrupting later removals - Timeout-bound watcher unsubscribe operations with a shared drain budget - Move JSON serialization of large usage caches from queue-time to write-time to avoid blocking main thread - Async tombstone + schedule history tree deletion instead of blocking recursive rmSync during GC, preventing main-thread stalls ~10s after startup * Extract usage cache writer into reusable durable snapshot class Consolidates serialized durable-write and generation-veto logic from three usage stores into UsageCacheSnapshotWriter. Eliminates duplication, centralizes multi-MB JSON serialization on the main thread via write-queue serialization, and vetoes superseded snapshots to avoid wasted rewrites. * fix(worktrees): prevent deletion from blocking Orca Worktree deletion used to recursively delete large session trees (hundreds of MB) on the critical path, stalling the event loop. Instead, rename trees into a `.pending-delete` tombstone queue and reclaim them asynchronously off the removal's critical path. Extracted host tree removal into a reusable helper (`removeHostTree`) that centralizes Windows retry logic. Added usage-cache flush on quit to prevent data loss when scans complete right before shutdown. Improved watcher removal deadline management with reserved tail slices for the final unsubscribe, and added retry logic for tombstone removals that fail once under transient Windows locking. * fix(history): retry failed session tree removals Tombstoned session trees whose removal fails transiently (e.g., EBUSY under Windows AV) are now re-queued in-process with bounded exponential backoff instead of sitting until the next HistoryManager construction. Prevents a single stuck tree from blocking the entire Orca process.
68 lines
2.6 KiB
TypeScript
68 lines
2.6 KiB
TypeScript
// Why: every recursive host delete Orca performs (worktrees, terminal history, quarantined recovery
|
|
// generations) hits the same Windows stickiness — AV/indexers/late handle releases surface transient
|
|
// EBUSY/ENOTEMPTY/EPERM on a tree Node just emptied. One helper so no call site forgets the retries.
|
|
|
|
import type { RmOptions } from 'node:fs'
|
|
import { rm } from 'node:fs/promises'
|
|
import { win32 } from 'node:path'
|
|
import { setTimeout as delay } from 'node:timers/promises'
|
|
|
|
const WINDOWS_REMOVE_RETRY_DELAYS_MS = [250, 500, 1_000, 2_000]
|
|
const WINDOWS_RM_MAX_RETRIES = 8
|
|
const WINDOWS_RM_RETRY_DELAY_MS = 150
|
|
|
|
export function toHostRemovalPath(targetPath: string): string {
|
|
// Why: Git for Windows can fail long recursive deletes even after Orca has
|
|
// proven the worktree target; Node's host deletion should use Win32 long paths.
|
|
return process.platform === 'win32' ? win32.toNamespacedPath(targetPath) : targetPath
|
|
}
|
|
|
|
function getHostRemovalOptions(): RmOptions {
|
|
const base = { recursive: true, force: true }
|
|
if (process.platform !== 'win32') {
|
|
return base
|
|
}
|
|
return {
|
|
...base,
|
|
// Why: large Windows trees commonly surface transient ENOTEMPTY/EPERM while
|
|
// Node walks and removes nested directories.
|
|
maxRetries: WINDOWS_RM_MAX_RETRIES,
|
|
retryDelay: WINDOWS_RM_RETRY_DELAY_MS
|
|
}
|
|
}
|
|
|
|
function isTransientWindowsRemovalError(error: unknown): boolean {
|
|
if (process.platform !== 'win32' || typeof error !== 'object' || error === null) {
|
|
return false
|
|
}
|
|
const code = 'code' in error && typeof error.code === 'string' ? error.code : undefined
|
|
if (code && ['EBUSY', 'ENOTEMPTY', 'EPERM'].includes(code)) {
|
|
return true
|
|
}
|
|
const message = 'message' in error && typeof error.message === 'string' ? error.message : ''
|
|
return /directory not empty|resource busy|operation not permitted/i.test(message)
|
|
}
|
|
|
|
/** Recursively remove a host directory tree, retrying the transient Windows failures. */
|
|
export async function removeHostTree(targetPath: string): Promise<void> {
|
|
const removalPath = toHostRemovalPath(targetPath)
|
|
const retryDelays = process.platform === 'win32' ? WINDOWS_REMOVE_RETRY_DELAYS_MS : []
|
|
const rmOptions = getHostRemovalOptions()
|
|
let attempt = 0
|
|
|
|
while (true) {
|
|
try {
|
|
await rm(removalPath, rmOptions)
|
|
return
|
|
} catch (error) {
|
|
if (attempt >= retryDelays.length || !isTransientWindowsRemovalError(error)) {
|
|
throw error
|
|
}
|
|
// Why: Git/Node recursive deletes on Windows can observe a just-emptied
|
|
// directory before antivirus/indexers/handles release it.
|
|
await delay(retryDelays[attempt])
|
|
attempt += 1
|
|
}
|
|
}
|
|
}
|