mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 16:03:26 +00:00
fix(codex): require provenance to skip migration scans and bound ignored launches
Drops the exit-match escape hatch that skipped a scan without route provenance, and ages out ignored reattach launches on the existing 60s/256 lease policy so a lost exit can no longer pin an active launch (and its completion marker) for the process lifetime. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
CODEX_SESSION_MIGRATION_LEASE_RETENTION_MS,
|
||||
evictStaleLeaseEntries
|
||||
} from './codex-session-migration-recent-exits'
|
||||
|
||||
type IgnoredLaunch = { recordedAt: number }
|
||||
|
||||
/** Lease ids whose reattach was proven away from the shared home, so their exit owes no scan. */
|
||||
export class CodexSessionMigrationIgnoredLaunches {
|
||||
private readonly launches = new Map<string, IgnoredLaunch>()
|
||||
|
||||
add(leaseId: string): void {
|
||||
const now = Date.now()
|
||||
this.launches.delete(leaseId)
|
||||
this.launches.set(leaseId, { recordedAt: now })
|
||||
evictStaleLeaseEntries(this.launches, now)
|
||||
}
|
||||
|
||||
has(leaseId: string): boolean {
|
||||
const launch = this.launches.get(leaseId)
|
||||
if (!launch) {
|
||||
return false
|
||||
}
|
||||
// Why: an entry whose exit never arrived must age out, or it absorbs a later exit and strands that launch active.
|
||||
if (Date.now() - launch.recordedAt > CODEX_SESSION_MIGRATION_LEASE_RETENTION_MS) {
|
||||
this.launches.delete(leaseId)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
delete(leaseId: string): boolean {
|
||||
return this.has(leaseId) && this.launches.delete(leaseId)
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,35 @@
|
||||
const RETENTION_MS = 60_000
|
||||
export const CODEX_SESSION_MIGRATION_LEASE_RETENTION_MS = 60_000
|
||||
const MAX_ENTRIES = 256
|
||||
|
||||
type RecentPtyExit = { sequence: number; recordedAt: number }
|
||||
|
||||
// Why: one aging policy for every lease-keyed migration map, so none can grow for the process lifetime.
|
||||
export function evictStaleLeaseEntries<T extends { recordedAt: number }>(
|
||||
entries: Map<string, T>,
|
||||
now: number
|
||||
): void {
|
||||
for (const [leaseId, entry] of entries) {
|
||||
if (now - entry.recordedAt > CODEX_SESSION_MIGRATION_LEASE_RETENTION_MS) {
|
||||
entries.delete(leaseId)
|
||||
}
|
||||
}
|
||||
while (entries.size > MAX_ENTRIES) {
|
||||
const oldestLeaseId = entries.keys().next().value
|
||||
if (oldestLeaseId === undefined) {
|
||||
break
|
||||
}
|
||||
entries.delete(oldestLeaseId)
|
||||
}
|
||||
}
|
||||
|
||||
export class CodexSessionMigrationRecentExits {
|
||||
private readonly exits = new Map<string, RecentPtyExit>()
|
||||
|
||||
record(leaseId: string, sequence: number): void {
|
||||
const now = Date.now()
|
||||
for (const [id, exit] of this.exits) {
|
||||
if (now - exit.recordedAt > RETENTION_MS) {
|
||||
this.exits.delete(id)
|
||||
}
|
||||
}
|
||||
this.exits.delete(leaseId)
|
||||
this.exits.set(leaseId, { sequence, recordedAt: now })
|
||||
while (this.exits.size > MAX_ENTRIES) {
|
||||
const oldestLeaseId = this.exits.keys().next().value
|
||||
if (oldestLeaseId === undefined) {
|
||||
break
|
||||
}
|
||||
this.exits.delete(oldestLeaseId)
|
||||
}
|
||||
evictStaleLeaseEntries(this.exits, now)
|
||||
}
|
||||
|
||||
consumeAfter(leaseId: string, startedSequence: number | undefined): RecentPtyExit | null {
|
||||
@@ -31,7 +39,7 @@ export class CodexSessionMigrationRecentExits {
|
||||
!exit ||
|
||||
startedSequence === undefined ||
|
||||
exit.sequence <= startedSequence ||
|
||||
Date.now() - exit.recordedAt > RETENTION_MS
|
||||
Date.now() - exit.recordedAt > CODEX_SESSION_MIGRATION_LEASE_RETENTION_MS
|
||||
) {
|
||||
return null
|
||||
}
|
||||
@@ -43,7 +51,7 @@ export class CodexSessionMigrationRecentExits {
|
||||
if (!exit) {
|
||||
return false
|
||||
}
|
||||
if (Date.now() - exit.recordedAt > RETENTION_MS) {
|
||||
if (Date.now() - exit.recordedAt > CODEX_SESSION_MIGRATION_LEASE_RETENTION_MS) {
|
||||
this.exits.delete(leaseId)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -572,7 +572,35 @@ describe('createCodexSessionMigrationScheduler', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('matches every late duplicate ignored callback without poisoning reuse', async () => {
|
||||
it('releases a stranded active launch once the ignored incarnation ages out', async () => {
|
||||
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
|
||||
const scheduler = createCodexSessionMigrationScheduler({
|
||||
isEligible: () => true,
|
||||
isQuitting: () => false,
|
||||
resolveSystemCodexHomePathOverride: () => undefined,
|
||||
startBackfill,
|
||||
startIndexHeal: vi.fn().mockResolvedValue(null),
|
||||
initialDelayMs: 1_000
|
||||
})
|
||||
|
||||
// The ignored incarnation never reports its exit, so only the newer launch ever finishes.
|
||||
scheduler.ignoreLaunch('stable-pty', 1)
|
||||
scheduler.beginLaunch('stable-pty', false, new Date(), 2)
|
||||
await vi.advanceTimersByTimeAsync(61_000)
|
||||
expect(startBackfill).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ writeCompletionMarker: false }),
|
||||
undefined
|
||||
)
|
||||
|
||||
scheduler.finishLaunch('stable-pty', 3)
|
||||
await vi.advanceTimersByTimeAsync(1_000)
|
||||
expect(startBackfill).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ writeCompletionMarker: true }),
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps late duplicate ignored callbacks from poisoning reuse', async () => {
|
||||
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
|
||||
const scheduler = createCodexSessionMigrationScheduler({
|
||||
isEligible: () => true,
|
||||
@@ -585,8 +613,8 @@ describe('createCodexSessionMigrationScheduler', () => {
|
||||
|
||||
scheduler.ignoreLaunch('stable-pty', 1)
|
||||
scheduler.finishLaunch('stable-pty', 3)
|
||||
expect(scheduler.matchesIgnoredLaunchExit('stable-pty', 2)).toBe(true)
|
||||
expect(scheduler.matchesIgnoredLaunchExit('stable-pty', 2)).toBe(true)
|
||||
scheduler.ignoreLaunch('stable-pty', 2)
|
||||
scheduler.ignoreLaunch('stable-pty', 2)
|
||||
scheduler.beginLaunch('stable-pty', false, new Date(), 4)
|
||||
await vi.advanceTimersByTimeAsync(1_000)
|
||||
expect(startBackfill).toHaveBeenCalledTimes(1)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getCodexSessionBackfillDate } from './codex-session-backfill-date'
|
||||
import { CodexSessionMigrationIgnoredLaunches } from './codex-session-migration-ignored-launches'
|
||||
import { CodexSessionMigrationRecentExits } from './codex-session-migration-recent-exits'
|
||||
import type {
|
||||
CodexSessionBackfillDate,
|
||||
@@ -18,7 +19,6 @@ export type CodexSessionMigrationScheduler = {
|
||||
startedSequence?: number
|
||||
): void
|
||||
ignoreLaunch(leaseId: string, startedSequence: number): void
|
||||
matchesIgnoredLaunchExit(leaseId: string, startedSequence: number): boolean
|
||||
finishLaunch(leaseId: string, exitSequence?: number): void
|
||||
scheduleInitialRun(): void
|
||||
scheduleRun(fullScanRequired?: boolean): void
|
||||
@@ -45,7 +45,7 @@ export function createCodexSessionMigrationScheduler(args: {
|
||||
let migrationTask: Promise<void> | null = null
|
||||
const activeLaunches = new Map<string, Date>()
|
||||
// Why: non-shared reattach exits must not masquerade as exit-before-begin races for a reused id.
|
||||
const ignoredLaunches = new Set<string>()
|
||||
const ignoredLaunches = new CodexSessionMigrationIgnoredLaunches()
|
||||
const ignoredPtyExits = new CodexSessionMigrationRecentExits()
|
||||
const earlyPtyExits = new CodexSessionMigrationRecentExits()
|
||||
let activeRunStopObserved = false
|
||||
@@ -216,9 +216,6 @@ export function createCodexSessionMigrationScheduler(args: {
|
||||
}
|
||||
ignoredLaunches.add(leaseId)
|
||||
},
|
||||
matchesIgnoredLaunchExit(leaseId, startedSequence): boolean {
|
||||
return ignoredPtyExits.matchesAfter(leaseId, startedSequence)
|
||||
},
|
||||
finishLaunch(leaseId, exitSequence): void {
|
||||
if (ignoredLaunches.delete(leaseId)) {
|
||||
if (exitSequence !== undefined) {
|
||||
|
||||
@@ -420,13 +420,6 @@ function handleCodexHomePtySpawned(args: {
|
||||
codexSessionMigration.ignoreLaunch(args.id, args.startedSequence)
|
||||
return
|
||||
}
|
||||
if (
|
||||
!paneAccount &&
|
||||
homeRoute === undefined &&
|
||||
codexSessionMigration?.matchesIgnoredLaunchExit(args.id, args.startedSequence)
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
const fullScanRequired =
|
||||
codexRuntimeHome?.beginHostSystemDefaultSessionMigrationLaunch(args.codexHomePath, {
|
||||
|
||||
Reference in New Issue
Block a user