mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 16:02:11 +00:00
* fix(opencode): read the binder's session store on the foreign SQLite reader worker (STA-9122) Before: the OpenCode session binder listed new sessions from opencode.db with node:sqlite on the main thread every 60 s (and on SessionStart kicks), so a large or contended store could stall the app the same way Cursor's did. After: the read is a pure openCodeBinderSessions reader in foreign-sqlite-readers/readers/, run only on the worker. The binder's correlation, pane snapshot and process sweep stay where they were. - The binder round awaits listSessions and re-checks its generation right after, so a stop() during the read discards the round before it touches the unbound map or the watermark. - The client's in-flight dedupe key now includes the cursor, so a stale round from before a restart cannot hand its rows to the restarted round. - Idle teardown is per reader. The binder lane keeps its thread for 120 s, longer than its 60 s poll, so the thread is not respawned every round. - A timeout, crash, malformed reply or unstartable worker resolves to [] (no sessions), the value the old read already returned on failure. - An absent store still reads as [] without a log line, and a permission or corrupt-file failure still logs (kept from #24577, now in the reader: it stats the path and throws anything but ENOENT/ENOTDIR to the client's log). - The binder lane inherits #24572's limits from the shared lane: no respawn until a timed-out worker has exited, 2 consecutive deaths, a queue cap of 8. Its timeout stays 60 s, matching its poll. - dispatch switches on the destructured kind, so a new kind without a case still fails to compile. orcad: the hook server runs there too, so orcad now ships foreign-sqlite-reader-entry.js beside orcad.js (ORCAD_ARTIFACTS, built as an orcad child). build-orcad runs a smoke check that starts the built worker under the build's Node and under the pinned runtime, and does a real binder read on a fixture DB, a Cursor read of a missing file and an OpenCode history list. The OpenCode history scanner uses the same entry and was bundled into orcad without it, so on orcad it always failed closed; it can now run. Tests: reader (cursor, same-ms ids, OpenCode 2 rows, missing then created, corrupt, inaccessible directory), retirement gate for the binder lane, dispatch routing, client lane (rows, failure -> [], dedupe per cursor, own thread, idle teardown default and override), binder loop with an async listSessions (failure -> [], stop during the read), orcad path resolution through orcad's host adapters, artifact list, and the smoke check against good, missing and non-reading entries. * test(opencode): cover the binder read deadline with fake timers and name the failure test accurately (STA-9122)
81 lines
3.3 KiB
JavaScript
81 lines
3.3 KiB
JavaScript
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join, resolve } from 'node:path'
|
|
import { describeProcessFailure, runProcessSync } from './script-child-process.mjs'
|
|
import { ORCAD_FOREIGN_SQLITE_READER_ENTRY } from '../../src/shared/orcad-artifacts.ts'
|
|
|
|
// Why a child process: the read must run under the runtime orcad ships, which may not be
|
|
// the Node running the build. The verdict is the exit code, never matched output.
|
|
const PROBE = `
|
|
const { Worker } = require('node:worker_threads')
|
|
const { DatabaseSync } = process.getBuiltinModule('node:sqlite')
|
|
const [entry, dbPath, missingPath] = process.argv.slice(2)
|
|
const db = new DatabaseSync(dbPath)
|
|
db.exec('CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT NOT NULL, time_created INTEGER NOT NULL, parent_id TEXT)')
|
|
db.prepare('INSERT INTO session VALUES (?, ?, ?, ?)').run('ses_smoke', '/smoke', 100, null)
|
|
db.close()
|
|
const steps = [
|
|
{
|
|
request: { id: 1, kind: 'openCodeBinderSessions', dbPath, cursor: { ms: 0, id: '' } },
|
|
expected: [{ id: 'ses_smoke', directory: '/smoke', createdAtMs: 100, parentId: null }]
|
|
},
|
|
{ request: { id: 2, kind: 'cursorProfile', dbPath: missingPath }, expected: { status: 'missing' } },
|
|
// The OpenCode history scanner's kinds share this entry.
|
|
{ request: { id: 3, kind: 'list', dbPaths: [], limit: null }, expected: { candidates: [], issues: [] } }
|
|
]
|
|
const worker = new Worker(entry, { execArgv: [] })
|
|
const fail = (code, message) => {
|
|
console.error(message)
|
|
process.exit(code)
|
|
}
|
|
setTimeout(() => fail(3, 'foreign SQLite reader worker did not answer'), 20000).unref()
|
|
worker.on('error', (error) => fail(4, String(error && error.stack || error)))
|
|
worker.on('exit', (code) => fail(5, 'foreign SQLite reader worker exited with ' + code))
|
|
let step = 0
|
|
worker.on('message', (response) => {
|
|
const { request, expected } = steps[step]
|
|
if (!response || response.id !== request.id || response.ok !== true ||
|
|
JSON.stringify(response.value) !== JSON.stringify(expected)) {
|
|
fail(6, request.kind + ' answered ' + JSON.stringify(response))
|
|
}
|
|
step += 1
|
|
if (step === steps.length) {
|
|
process.exit(0)
|
|
}
|
|
worker.postMessage(steps[step].request)
|
|
})
|
|
worker.postMessage(steps[0].request)
|
|
`
|
|
|
|
/**
|
|
* Load the built foreign SQLite reader entry and run real reads through it.
|
|
* @param outDir - orcad output directory holding the entry.
|
|
* @param options.runtimePath - Node to run under; the build's own Node when omitted.
|
|
*/
|
|
export function smokeForeignSqliteReaderWorker(outDir, { runtimePath, timeoutMs = 30_000 } = {}) {
|
|
const directory = mkdtempSync(join(tmpdir(), 'orca-foreign-sqlite-smoke-'))
|
|
try {
|
|
const probe = join(directory, 'probe.cjs')
|
|
writeFileSync(probe, PROBE)
|
|
const result = runProcessSync({
|
|
program: runtimePath ?? process.execPath,
|
|
args: [
|
|
probe,
|
|
resolve(outDir, ORCAD_FOREIGN_SQLITE_READER_ENTRY),
|
|
join(directory, 'opencode.db'),
|
|
join(directory, 'missing.vscdb')
|
|
],
|
|
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
|
|
timeoutMs,
|
|
maxOutputBytes: 64 * 1024
|
|
})
|
|
if (result.code !== 0 || result.timedOut) {
|
|
throw new Error(
|
|
`Foreign SQLite reader worker smoke failed: ${describeProcessFailure(result)}`
|
|
)
|
|
}
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true })
|
|
}
|
|
}
|