mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 08:04:05 +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.
62 lines
2.5 KiB
TypeScript
62 lines
2.5 KiB
TypeScript
import { mkdir } from 'node:fs/promises'
|
|
import { dirname } from 'node:path'
|
|
import {
|
|
durableWriteTempPath,
|
|
removeStaleDurableWriteTempFiles,
|
|
writeFileDurableIfCurrent
|
|
} from './durable-file-write'
|
|
|
|
/**
|
|
* Serialized durable writer for the per-provider usage caches. One per store: the multi-MB JSON must
|
|
* not block the Electron main thread, writes are queued so two overlapping renames can't both veto
|
|
* themselves, and a snapshot superseded before its turn is dropped instead of paying for a rewrite.
|
|
*/
|
|
export class UsageCacheSnapshotWriter {
|
|
private generation = 0
|
|
private pending: Promise<void>
|
|
|
|
constructor(
|
|
private readonly logTag: string,
|
|
private readonly resolveFile: () => string
|
|
) {
|
|
// Why: a crash between write and rename orphans a multi-MB temp file; reclaim once per launch.
|
|
// Seeding the write queue with it also orders the sweep ahead of the first write of this launch.
|
|
this.pending = removeStaleDurableWriteTempFiles(resolveFile())
|
|
}
|
|
|
|
/** `serialize` runs only if this snapshot is still the newest one when its turn comes. */
|
|
write(serialize: () => string): Promise<void> {
|
|
const generation = ++this.generation
|
|
const write = this.pending.then(() => this.commit(generation, serialize))
|
|
// Why: keep the queue usable after a failure — the awaiting caller still sees the rejection.
|
|
this.pending = write.catch((error: unknown) => {
|
|
console.error(`${this.logTag} Failed to persist usage cache:`, error)
|
|
})
|
|
return write
|
|
}
|
|
|
|
/** Await every write queued so far. Never rejects — the queue already logs its own failures. */
|
|
flush(): Promise<void> {
|
|
return this.pending
|
|
}
|
|
|
|
private async commit(generation: number, serialize: () => string): Promise<void> {
|
|
// Why: a newer snapshot is already queued behind this one, so rewriting the cache here is wasted.
|
|
if (generation !== this.generation) {
|
|
return
|
|
}
|
|
// Why serialize here and not at queue time: it blocks the main process for a multi-MB cache, and
|
|
// superseded generations must not pay for it. Synchronous, so no mutation can tear the JSON.
|
|
const payload = serialize()
|
|
const usageFile = this.resolveFile()
|
|
await mkdir(dirname(usageFile), { recursive: true }).catch(() => {})
|
|
// Why: atomic temp-file + durable rename so a crash cannot leave a truncated analytics file.
|
|
await writeFileDurableIfCurrent(
|
|
durableWriteTempPath(usageFile),
|
|
usageFile,
|
|
payload,
|
|
() => generation === this.generation
|
|
)
|
|
}
|
|
}
|