Fix Codex status after Ctrl+C copy and side-chat navigation (#24339)

* fix: preserve Codex status on ambiguous Ctrl+C input

* fix: confirm Codex turn cancellations from host rollout records

* fix: keep ephemeral side hooks separate from the Codex main turn

* perf: watch active Codex rollouts and skip unrelated records

* fix: retain confirmed Codex cancellation across late relay events
This commit is contained in:
Neil
2026-10-01 17:33:58 -07:00
committed by GitHub
parent 6e7e964705
commit 02790c53e8
31 changed files with 1121 additions and 222 deletions
+20 -4
View File
@@ -8,6 +8,10 @@ import {
type JsonRecord
} from './codex-rollout-jsonl-cursor'
import { reconcileCodexTranscriptTurn, type CodexTranscriptTurn } from './codex-turn-transcript'
import { isCodexStatusTranscriptLine } from './codex-status-transcript-line'
import { readApprovalsReviewer } from './codex-subagent-reviewer'
import type { CodexApprovalsReviewer } from './codex-subagent-reviewer'
@@ -34,6 +38,7 @@ type TrackedTranscriptSubagent = JsonlCursor & {
export type CodexSubagentTranscriptState = {
parent: JsonlCursor
rootTurn: CodexTranscriptTurn
subagents: Map<string, TrackedTranscriptSubagent>
/** Incremental reviewer cursors for child rollouts, which must not replace the parent cursor. */
reviewerCursorsByPath: Map<string, JsonlCursor>
@@ -175,6 +180,7 @@ function childIsComplete(records: JsonRecord[]): boolean {
export function createCodexSubagentTranscriptState(): CodexSubagentTranscriptState {
return {
parent: { offset: 0, carry: '' },
rootTurn: { interrupted: false },
subagents: new Map(),
reviewerCursorsByPath: new Map(),
reviewersByPath: new Map()
@@ -191,23 +197,30 @@ export function reconcileCodexSubagentTranscript(
state: CodexSubagentTranscriptState,
roster: CodexSubagentRoster,
transcriptPath: string | undefined
): void {
): boolean {
const normalizedPath = normalizedTranscriptPath(transcriptPath)
if (!normalizedPath) {
return
return false
}
let changed = false
if (state.parent.filePath !== normalizedPath) {
for (const id of state.subagents.keys()) {
finishCodexSubagent(roster, id)
}
changed = true
state.parent = { filePath: normalizedPath, offset: 0, carry: '' }
state.rootTurn = { interrupted: false }
state.subagents.clear()
state.reviewerCursorsByPath.clear()
state.reviewersByPath.clear()
// Why: a different rollout is a different session, so its predecessor's reviewer is void.
state.approvalsReviewer = undefined
}
const parentRecords = readJsonlCursor(state.parent)
const parentRecords = readJsonlCursor(state.parent, isCodexStatusTranscriptLine)
changed ||=
Boolean(parentRecords?.length) ||
(parentRecords === undefined && state.approvalsReviewer !== undefined)
reconcileCodexTranscriptTurn(state.rootTurn, parentRecords ?? [])
// A stale reviewer must never turn an unreadable rollout into a hidden prompt.
state.approvalsReviewer =
parentRecords === undefined
@@ -248,7 +261,8 @@ export function reconcileCodexSubagentTranscript(
entriesByDirectory
)
}
const records = readJsonlCursor(tracked)
const records = readJsonlCursor(tracked, isCodexStatusTranscriptLine)
changed ||= Boolean(records?.length)
if (!records) {
// Why: a rollout that never appears (or is deleted) has no completion event, so time-box it instead of leaking a working row.
tracked.filePath = undefined
@@ -267,7 +281,9 @@ export function reconcileCodexSubagentTranscript(
continue
}
}
changed = true
finishCodexSubagent(roster, id)
state.subagents.delete(id)
}
return changed
}