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:
Neil
2026-08-11 00:23:49 -07:00
co-authored by Orca
parent 0a8e2b17df
commit 2b15f51887
5 changed files with 91 additions and 30 deletions
@@ -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) {
-7
View File
@@ -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, {