mirror of
https://github.com/stablyai/orca.git
synced 2026-10-13 00:02:54 +00:00
* fix: preserve Codex status on ambiguous Ctrl+C input * fix: confirm Codex turn cancellations from host rollout records * fix: keep ephemeral side hooks separate from the Codex main turn * perf: watch active Codex rollouts and skip unrelated records * fix: retain confirmed Codex cancellation across late relay events
92 lines
2.6 KiB
TypeScript
92 lines
2.6 KiB
TypeScript
import { watch, type FSWatcher } from 'node:fs'
|
|
import { basename, dirname } from 'node:path'
|
|
|
|
export type TranscriptNativeWatcher = {
|
|
/** Best-effort bind; false keeps the caller in reconciliation-only mode. */
|
|
bind: () => boolean
|
|
/** Detach from an identity that may no longer represent the watched path. */
|
|
invalidate: () => void
|
|
needsRebind: () => boolean
|
|
dispose: () => void
|
|
}
|
|
|
|
/**
|
|
* Optional fs.watch acceleration for transcript reconciliation. Native watches
|
|
* can fail on otherwise-readable remote filesystems, so binding is retryable
|
|
* and never owns transcript liveness; the caller's polling loop does.
|
|
*/
|
|
export function createTranscriptNativeWatcher(
|
|
filePath: string,
|
|
onEvent: () => void,
|
|
onError: () => void,
|
|
options: { watchParent?: boolean } = {}
|
|
): TranscriptNativeWatcher {
|
|
const watchedName = basename(filePath)
|
|
let disposed = false
|
|
let watcher: FSWatcher | null = null
|
|
let rebindNeeded = true
|
|
|
|
function invalidateCandidate(candidate: FSWatcher): void {
|
|
if (watcher !== candidate) {
|
|
return
|
|
}
|
|
watcher = null
|
|
rebindNeeded = true
|
|
candidate.close()
|
|
}
|
|
|
|
return {
|
|
bind(): boolean {
|
|
if (disposed || watcher) {
|
|
return watcher !== null
|
|
}
|
|
let nextWatcher: FSWatcher
|
|
try {
|
|
// Parent watches survive replacement; append-only readers can watch one file to avoid sibling events.
|
|
nextWatcher = watch(
|
|
options.watchParent === false ? filePath : dirname(filePath),
|
|
(event, changedName) => {
|
|
if (changedName !== null && changedName.toString() !== watchedName) {
|
|
return
|
|
}
|
|
// Why: a parent replacement may emit rename without a watcher error.
|
|
if (event === 'rename') {
|
|
invalidateCandidate(nextWatcher)
|
|
}
|
|
onEvent()
|
|
}
|
|
)
|
|
} catch {
|
|
rebindNeeded = true
|
|
return false
|
|
}
|
|
// Why: an active tail should not keep a headless runtime alive during shutdown.
|
|
nextWatcher.unref?.()
|
|
nextWatcher.on('error', () => {
|
|
if (disposed || watcher !== nextWatcher) {
|
|
return
|
|
}
|
|
invalidateCandidate(nextWatcher)
|
|
onError()
|
|
})
|
|
watcher = nextWatcher
|
|
rebindNeeded = false
|
|
return true
|
|
},
|
|
invalidate(): void {
|
|
if (watcher) {
|
|
invalidateCandidate(watcher)
|
|
} else {
|
|
rebindNeeded = true
|
|
}
|
|
},
|
|
needsRebind: () => rebindNeeded,
|
|
dispose(): void {
|
|
disposed = true
|
|
watcher?.close()
|
|
watcher = null
|
|
rebindNeeded = false
|
|
}
|
|
}
|
|
}
|