mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 00:03:59 +00:00
* fix(codex): stop re-scanning all Codex session history on every launch (#16251) A launch deleted the backfill completion marker, and a marker could never be written while a Codex pane was open, so every launch re-derived "needs full scan" and walked the entire .codex/sessions tree — on Windows with a large history that read as a hung window. - v4 marker keeps a durable full-history baseline plus a bounded set of pending dates. v3 is read as a baseline, so upgrades pay no full scan. - A launch now marks dates pending instead of deleting the marker, and a full pass certifies the baseline even while a pane is still running; the live pane's own date just stays pending. - Pending dates are persisted, so an abnormal exit or a cross-midnight pane recovers a bounded window instead of a full walk. - A date-limited pass can only extend an existing baseline, never create one, so it can no longer certify history it never looked at. - Marker and index-heal target roots compare through normalizeRuntimePathForComparison, so Windows spellings of one directory stop invalidating each other. - Both append-only ledgers stream instead of readFileSync + whole-file JSON.parse, keeping the main thread responsive on large histories. * fix(codex): keep the backfill marker's full-scan demand durable Review follow-ups on the v4 backfill marker: - markCodexSessionBackfillMarkerPending no longer erases a persisted needsFullScan; the demand survives until a generation-current full walk retires it, and the function now reports it so the launch path folds it into its own in-memory flag (as @rumoii's #16252 does). - A full pass settles the whole pending set instead of subtracting the empty set, so a date a full walk provably covered stops forcing an extra bounded pass on every startup. - isCodexSessionBackfillDate does a real calendar check, so a corrupted marker cannot carry 2026/99/99. No age or future bound: the same guard gates rollout publication and a clock-skewed directory holds real sessions. - 'scans only the current date once a baseline exists' now has a second date directory, so it fails on a full walk instead of passing either way.
113 lines
4.2 KiB
TypeScript
113 lines
4.2 KiB
TypeScript
import type { CodexSessionBackfillDate } from './codex-session-backfill-types'
|
|
|
|
// Set algebra over the UTC date directories that make up a bounded backfill
|
|
// pass. Kept apart from the walk itself so the durable marker, the scheduler,
|
|
// and the pass all agree on identity, ordering, and range expansion.
|
|
|
|
export function getCodexSessionBackfillDate(date = new Date()): CodexSessionBackfillDate {
|
|
return [
|
|
String(date.getUTCFullYear()).padStart(4, '0'),
|
|
String(date.getUTCMonth() + 1).padStart(2, '0'),
|
|
String(date.getUTCDate()).padStart(2, '0')
|
|
]
|
|
}
|
|
|
|
export function isCodexSessionBackfillDate(value: unknown): value is CodexSessionBackfillDate {
|
|
if (!Array.isArray(value) || value.length !== 3) {
|
|
return false
|
|
}
|
|
const key = value.join('-')
|
|
// Why: shape alone accepts directories the calendar never produces (2026/99/99
|
|
// from a corrupted marker, 2025/02/29); a UTC round-trip rejects them for free.
|
|
// Deliberately no age or future bound — this also gates which managed rollouts
|
|
// get published, and a clock-skewed future directory holds real sessions.
|
|
return (
|
|
/^\d{4}-\d{2}-\d{2}$/.test(key) &&
|
|
toCodexSessionBackfillDateKey(getCodexSessionBackfillDate(toUtcDate(value))) === key
|
|
)
|
|
}
|
|
|
|
export function toCodexSessionBackfillDateKey(date: CodexSessionBackfillDate): string {
|
|
return date.join('-')
|
|
}
|
|
|
|
export function compareCodexSessionBackfillDates(
|
|
left: CodexSessionBackfillDate,
|
|
right: CodexSessionBackfillDate
|
|
): number {
|
|
return toCodexSessionBackfillDateKey(left).localeCompare(toCodexSessionBackfillDateKey(right))
|
|
}
|
|
|
|
/** Deduplicated ascending union; invalid entries are dropped. */
|
|
export function mergeCodexSessionBackfillDates(
|
|
...groups: readonly (readonly CodexSessionBackfillDate[] | undefined)[]
|
|
): CodexSessionBackfillDate[] {
|
|
const merged = new Map<string, CodexSessionBackfillDate>()
|
|
for (const group of groups) {
|
|
for (const date of group ?? []) {
|
|
if (isCodexSessionBackfillDate(date)) {
|
|
merged.set(toCodexSessionBackfillDateKey(date), date)
|
|
}
|
|
}
|
|
}
|
|
return [...merged.values()].sort(compareCodexSessionBackfillDates)
|
|
}
|
|
|
|
export function subtractCodexSessionBackfillDates(
|
|
dates: readonly CodexSessionBackfillDate[],
|
|
removed: readonly CodexSessionBackfillDate[]
|
|
): CodexSessionBackfillDate[] {
|
|
const removedKeys = new Set(removed.map(toCodexSessionBackfillDateKey))
|
|
return dates.filter((date) => !removedKeys.has(toCodexSessionBackfillDateKey(date)))
|
|
}
|
|
|
|
/** Reads persisted marker dates; anything unrecognized is discarded. */
|
|
export function parseCodexSessionBackfillDates(value: unknown): CodexSessionBackfillDate[] {
|
|
return Array.isArray(value)
|
|
? mergeCodexSessionBackfillDates(value.filter(isCodexSessionBackfillDate))
|
|
: []
|
|
}
|
|
|
|
export function getCodexSessionBackfillDatesBetween(
|
|
startedAt: Date,
|
|
finishedAt: Date
|
|
): CodexSessionBackfillDate[] {
|
|
const dates: CodexSessionBackfillDate[] = []
|
|
const cursor = toUtcMidnight(startedAt)
|
|
const last = toUtcMidnight(finishedAt)
|
|
while (cursor <= last) {
|
|
dates.push(getCodexSessionBackfillDate(cursor))
|
|
cursor.setUTCDate(cursor.getUTCDate() + 1)
|
|
}
|
|
return dates
|
|
}
|
|
|
|
/**
|
|
* Widens a pending set into the contiguous range that ends at `today`.
|
|
*
|
|
* Why: an abnormal exit or a pane held open across midnight leaves activity on
|
|
* dates nobody got to record. The gap between the oldest pending date and today
|
|
* is the smallest window that provably contains them. Returns null once that
|
|
* window outgrows `maxDates`, where a full walk is the cheaper certainty.
|
|
*/
|
|
export function expandCodexSessionBackfillDatesThroughToday(
|
|
dates: readonly CodexSessionBackfillDate[],
|
|
today: CodexSessionBackfillDate,
|
|
maxDates: number
|
|
): CodexSessionBackfillDate[] | null {
|
|
if (dates.length === 0) {
|
|
return []
|
|
}
|
|
const bounds = mergeCodexSessionBackfillDates(dates, [today])
|
|
const range = getCodexSessionBackfillDatesBetween(toUtcDate(bounds[0]), toUtcDate(bounds.at(-1)!))
|
|
return range.length > maxDates ? null : range
|
|
}
|
|
|
|
function toUtcDate([year, month, day]: readonly string[]): Date {
|
|
return new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)))
|
|
}
|
|
|
|
function toUtcMidnight(date: Date): Date {
|
|
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()))
|
|
}
|