mirror of
https://github.com/stablyai/orca.git
synced 2026-10-04 16:02:08 +00:00
* fix(codex): index a new account home before bridging history into it Codex indexes every rollout present when it first creates its state DB, and the TUI gives up after 30s, so large histories broke a new account's launch. Fixes #20669 * test(codex): keep the account migration test from starting the real Codex binary Selecting an account starts the history bridge, which spawned codex app-server on the fixture homes and raced the test's temp-dir cleanup. * fix(codex): index a new account's most recent bridged history first Indexing a large history takes minutes, and Codex's /resume hides unindexed threads once a directory has any indexed one, so recent conversations stayed missing until the heal reached them in directory-listing order. * test(codex): cover the history bridge's quit, no-history and failed-link guards Drop the stop check before creating the state DB: it ran in the same tick as the check at bridge start, so it could never observe a quit. * test(codex): make the account heal tests fail closed instead of reaching a real codex Fake invocations now point at a nonexistent binary and a throwaway CODEX_HOME, and cover per-home failure memory and a session that fails during quit. --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
151 lines
4.5 KiB
TypeScript
151 lines
4.5 KiB
TypeScript
import { readdirSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import SyncDatabase from '../sqlite/sync-database'
|
|
|
|
const STATE_DB_FILE_PATTERN = /^state_(\d+)\.sqlite$/
|
|
|
|
export type CodexStateDbBackfillStatus =
|
|
| { kind: 'complete'; stateDbPath: string }
|
|
| { kind: 'incomplete'; stateDbPath: string; status: string }
|
|
| { kind: 'missing' }
|
|
| { kind: 'not-tracked'; stateDbPath: string }
|
|
| { kind: 'unreadable'; stateDbPath: string; error: string }
|
|
|
|
export function findNewestCodexStateDbPath(codexHomePath: string): string | null {
|
|
let entries: string[]
|
|
try {
|
|
entries = readdirSync(codexHomePath)
|
|
} catch {
|
|
return null
|
|
}
|
|
let newest: { version: number; name: string } | null = null
|
|
for (const name of entries) {
|
|
const match = STATE_DB_FILE_PATTERN.exec(name)
|
|
if (!match) {
|
|
continue
|
|
}
|
|
const version = Number(match[1])
|
|
if (!newest || version > newest.version) {
|
|
newest = { version, name }
|
|
}
|
|
}
|
|
return newest ? join(codexHomePath, newest.name) : null
|
|
}
|
|
|
|
/** Reads Codex-owned backfill metadata without creating or mutating its database. */
|
|
export function readCodexStateDbBackfillStatus(codexHomePath: string): CodexStateDbBackfillStatus {
|
|
const stateDbPath = findNewestCodexStateDbPath(codexHomePath)
|
|
if (!stateDbPath) {
|
|
return { kind: 'missing' }
|
|
}
|
|
let db: SyncDatabase | null = null
|
|
try {
|
|
db = new SyncDatabase(stateDbPath, { readonly: true, fileMustExist: true })
|
|
const table = db
|
|
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'backfill_state'")
|
|
.get()
|
|
if (!table) {
|
|
return { kind: 'not-tracked', stateDbPath }
|
|
}
|
|
const row = db.prepare('SELECT status FROM backfill_state WHERE id = 1').get() as
|
|
| { status?: unknown }
|
|
| undefined
|
|
if (!row || typeof row.status !== 'string') {
|
|
return { kind: 'not-tracked', stateDbPath }
|
|
}
|
|
return row.status === 'complete'
|
|
? { kind: 'complete', stateDbPath }
|
|
: { kind: 'incomplete', stateDbPath, status: row.status }
|
|
} catch (error) {
|
|
return {
|
|
kind: 'unreadable',
|
|
stateDbPath,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
}
|
|
} finally {
|
|
try {
|
|
db?.close()
|
|
} catch {
|
|
// A close failure cannot change the read-only result already collected.
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lower-cased thread ids already in Codex's state DB, or null when the DB is
|
|
* missing or unreadable. Read-only; never creates or mutates Codex's database.
|
|
*/
|
|
export function readIndexedCodexThreadIds(codexHomePath: string): Set<string> | null {
|
|
const stateDbPath = findNewestCodexStateDbPath(codexHomePath)
|
|
if (!stateDbPath) {
|
|
return null
|
|
}
|
|
let db: SyncDatabase | null = null
|
|
try {
|
|
db = new SyncDatabase(stateDbPath, { readonly: true, fileMustExist: true })
|
|
const ids = new Set<string>()
|
|
for (const row of db.prepare('SELECT id FROM threads').all()) {
|
|
if (typeof row.id === 'string') {
|
|
ids.add(row.id.toLowerCase())
|
|
}
|
|
}
|
|
return ids
|
|
} catch (error) {
|
|
console.warn('[codex-state-db] Failed to read indexed Codex threads:', error)
|
|
return null
|
|
} finally {
|
|
try {
|
|
db?.close()
|
|
} catch {
|
|
// A close failure cannot change the read-only result already collected.
|
|
}
|
|
}
|
|
}
|
|
|
|
export function countCodexSessionFilesUpTo(sessionsRoot: string, limit: number): number {
|
|
let count = 0
|
|
const pendingDirectories = [sessionsRoot]
|
|
while (pendingDirectories.length > 0 && count < limit) {
|
|
const directory = pendingDirectories.pop() as string
|
|
let entries
|
|
try {
|
|
entries = readdirSync(directory, { withFileTypes: true })
|
|
} catch {
|
|
continue
|
|
}
|
|
for (const entry of entries) {
|
|
if (entry.isDirectory()) {
|
|
pendingDirectories.push(join(directory, entry.name))
|
|
} else if (
|
|
entry.isFile() &&
|
|
// Why: Codex's startup backfill parses compressed rollouts too.
|
|
(entry.name.endsWith('.jsonl') || entry.name.endsWith('.jsonl.zst'))
|
|
) {
|
|
count += 1
|
|
if (count >= limit) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
export const BACKFILL_PENDING_MIN_SESSION_FILES = 100
|
|
|
|
export function isCodexStateDbBackfillPending(codexHomePath: string): boolean {
|
|
const status = readCodexStateDbBackfillStatus(codexHomePath)
|
|
if (status.kind === 'incomplete') {
|
|
return true
|
|
}
|
|
if (status.kind !== 'missing' && status.kind !== 'not-tracked') {
|
|
return false
|
|
}
|
|
return (
|
|
countCodexSessionFilesUpTo(
|
|
join(codexHomePath, 'sessions'),
|
|
BACKFILL_PENDING_MIN_SESSION_FILES
|
|
) >= BACKFILL_PENDING_MIN_SESSION_FILES
|
|
)
|
|
}
|