Files
orca/src/main/ai-vault/session-scanner-opencode-sqlite-scan-scope.ts
T
f0b5b8566c Bound OpenCode history reads and repeated worker failures (#25292)
* fix(opencode): keep scan budgets across queue waits and batches

Reuse the scan-owned lifetime proposed in #10708 by @AmethystLiang with the existing shared worker queue.

* test(opencode): check nonempty session fixtures and lint scoped controls

* Derive OpenCode scan deadline message from its budget

---------

Co-authored-by: Neil Parker <nwparker@MacBook-Pro-3.localdomain>
Co-authored-by: OpenCode issue campaign <codex@localhost>
2026-10-04 14:31:22 -07:00

76 lines
2.4 KiB
TypeScript

import { AsyncLocalStorage } from 'node:async_hooks'
import { throwIfSignalAborted } from '../../shared/abort-signal-reason'
import type { WorkerThreadRequestOwner } from '../worker-thread-request-queue'
export const OPENCODE_SQLITE_SCAN_BUDGET_MS = 45_000
class OpenCodeSqliteScanScope implements WorkerThreadRequestOwner {
private readonly controller = new AbortController()
readonly signal = this.controller.signal
private remainingMs = OPENCODE_SQLITE_SCAN_BUDGET_MS
private outstanding = 0
private armedAt = 0
private timer: NodeJS.Timeout | undefined
async run<T>(
callerSignal: AbortSignal | undefined,
fn: (signal: AbortSignal, owner: WorkerThreadRequestOwner) => Promise<T>
): Promise<T> {
throwIfSignalAborted(callerSignal)
throwIfSignalAborted(this.signal)
if (this.outstanding++ === 0) {
this.armedAt = Date.now()
this.timer = setTimeout(() => {
const error = new Error(
`OpenCode SQLite scan exceeded its ${OPENCODE_SQLITE_SCAN_BUDGET_MS / 1000}s work budget`
)
error.name = 'OpenCodeSqliteScanDeadlineError'
this.controller.abort(error)
}, this.remainingMs)
this.timer.unref?.()
}
const signal = callerSignal ? AbortSignal.any([callerSignal, this.signal]) : this.signal
try {
return await fn(signal, this)
} finally {
if (--this.outstanding === 0) {
this.pause()
}
}
}
dispose(): void {
this.pause()
this.controller.abort(new Error('OpenCode SQLite scan ended'))
}
private pause(): void {
if (this.timer) {
clearTimeout(this.timer)
this.timer = undefined
this.remainingMs = Math.max(0, this.remainingMs - (Date.now() - this.armedAt))
}
}
}
const scanScope = new AsyncLocalStorage<OpenCodeSqliteScanScope>()
export async function withOpenCodeSqliteScanScope<T>(fn: () => Promise<T>): Promise<T> {
const scope = new OpenCodeSqliteScanScope()
try {
return await scanScope.run(scope, fn)
} finally {
scope.dispose()
}
}
// The clock covers outstanding SQLite work, including admission and WSL preparation.
export function runOpenCodeSqliteScanRequest<T>(
signal: AbortSignal | undefined,
fn: (signal?: AbortSignal, owner?: WorkerThreadRequestOwner) => Promise<T>,
agent?: 'opencode2' | 'zcode' | 'native-chat'
): Promise<T> {
const scope = agent === 'zcode' || agent === 'native-chat' ? undefined : scanScope.getStore()
return scope ? scope.run(signal, fn) : fn(signal)
}