Merge remote-tracking branch 'origin/brennanb2025/structured-holder-and-entry-fixes' into brennanb2025/structured-session-eviction

This commit is contained in:
Brennan Benson
2026-08-23 17:51:58 -07:00
13 changed files with 310 additions and 27 deletions
@@ -21,6 +21,17 @@ describe('Codex TUI rollout proof', () => {
expect(parseCodexTuiStatusSessionId('Session: not-a-session')).toBeNull()
})
it('parses the Codex 0.148 status screen and semantic thread labels', () => {
const status = [
'\u001b[2m│ >_ OpenAI Codex (v0.148.0) │',
'│ Model: gpt-5.6-sol (reasoning high, summaries auto) │',
`│ Session: \u001b[22m${THREAD}\u001b[2m │`
].join('\n')
expect(parseCodexTuiStatusSessionId(status)).toBe(THREAD)
expect(parseCodexTuiStatusSessionId(`Thread ID: ${OTHER_THREAD}`)).toBe(OTHER_THREAD)
})
it('uses Kitty Enter only while the TUI negotiated Kitty input', () => {
expect(codexTuiStatusProbeInput(0)).toEqual({
command: '\u001b[200~/status\u001b[201~',
@@ -151,4 +162,33 @@ describe('Codex TUI rollout proof', () => {
'\u001b'
])
})
it('accepts a proven blank Codex 0.148 session before its lazy rollout exists', async () => {
let reads = 0
const write = vi.fn((_data: string) => true)
const resolveRollout = vi.fn(async () => null)
await expect(
resolveLiveCodexTuiRollout({
codexHome: '/pinned',
kittyKeyboardFlags: 1,
readOutput: () => ({
text:
reads++ > 0
? `│ >_ OpenAI Codex (v0.148.0) │\n│ Session: \u001b[22m${THREAD}\u001b[2m │`
: '',
lastOutputAt: reads > 1 ? 2 : 1
}),
write,
resolveRollout,
delay: async () => undefined
})
).resolves.toEqual({ threadId: THREAD })
expect(resolveRollout).toHaveBeenCalledTimes(5)
expect(write.mock.calls.map(([data]) => data)).toEqual([
'\u001b[200~/status\u001b[201~',
'\u001b[13u',
'\u001b'
])
})
})
+25 -9
View File
@@ -5,7 +5,10 @@ import { relativePathInsideRoot } from '../../shared/cross-platform-path'
import { listCodexSessionJsonlFilesIncrementally } from './codex-session-file-listing'
const SESSION_ID_PATTERN = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
const STATUS_SESSION_RE = new RegExp(`\\bSession(?: ID)?\\s*:\\s*(${SESSION_ID_PATTERN})\\b`, 'gi')
const STATUS_SESSION_RE = new RegExp(
`\\b(?:Session|Thread)(?:\\s+ID)?\\s*:\\s*(${SESSION_ID_PATTERN})\\b`,
'gi'
)
const ROLLOUT_READ_LIMIT = 64 * 1024
const STATUS_COMMAND_PASTE = '\u001b[200~/status\u001b[201~'
const KITTY_ENTER = '\u001b[13u'
@@ -125,7 +128,7 @@ export async function resolveLiveCodexTuiRollout(input: {
timeoutMs?: number
resolveRollout?: (codexHome: string, threadId: string) => Promise<string | null>
delay?: (ms: number) => Promise<void>
}): Promise<{ threadId: string; transcriptPath: string }> {
}): Promise<{ threadId: string; transcriptPath?: string }> {
const baselineOutputAt = input.readOutput().lastOutputAt
const probe = codexTuiStatusProbeInput(input.kittyKeyboardFlags)
if (!input.write(probe.command)) {
@@ -144,14 +147,19 @@ export async function resolveLiveCodexTuiRollout(input: {
const threadId = parseCodexTuiStatusSessionId(output.text)
if (threadId) {
const resolveRollout = input.resolveRollout ?? resolvePinnedCodexRolloutProof
const transcriptPath = await resolveRollout(input.codexHome, threadId)
let transcriptPath: string | null = null
// Codex 0.148 allocates a session before it writes a rollout; give a just-written
// file a short visibility window without rejecting a genuinely blank conversation.
for (let attempt = 0; attempt < 5 && !transcriptPath; attempt += 1) {
transcriptPath = await resolveRollout(input.codexHome, threadId)
if (!transcriptPath && attempt < 4) {
await delay(100)
}
}
if (!input.write('\u001b')) {
throw new Error('The agent terminal could not finish Codex session verification.')
}
if (!transcriptPath) {
throw new Error('Could not find the rollout for this Codex conversation.')
}
return { threadId, transcriptPath }
return { threadId, ...(transcriptPath ? { transcriptPath } : {}) }
}
}
await delay(100)
@@ -178,12 +186,20 @@ async function readCodexRolloutSessionMetaId(filePath: string): Promise<string |
const record = JSON.parse(firstLine) as {
type?: unknown
id?: unknown
payload?: { id?: unknown }
session_id?: unknown
thread_id?: unknown
payload?: { id?: unknown; session_id?: unknown; thread_id?: unknown }
}
if (record.type !== 'session_meta') {
return null
}
const id = record.payload?.id ?? record.id
const id =
record.payload?.id ??
record.payload?.session_id ??
record.payload?.thread_id ??
record.id ??
record.session_id ??
record.thread_id
return typeof id === 'string' && id.length > 0 ? id : null
} catch {
return null
@@ -55,6 +55,8 @@ describe('structured session cold restoration', () => {
it('normalizes a restored tab id and removes it when closed', async () => {
const runtime = new OrcaRuntimeService()
const closeSessionTab = vi.fn(async () => undefined)
runtime.setNotifier({ closeSessionTab } as never)
const internal = runtime as unknown as {
getKnownWorkspaceSessionWorktreeIds(): Set<string>
hydrateHeadlessMobileSessionTabsFromWorkspaceSession(): Set<string>
@@ -146,6 +148,11 @@ describe('structured session cold restoration', () => {
reason: 'user'
})
expect(closeSessionTab).toHaveBeenCalledWith(
'structured-agent-session-restored-session',
'workspace-1'
)
const closed = await runtime.listMobileSessionTabs('id:workspace-1')
expect(closed.tabs.map((tab) => tab.id)).toEqual([
'terminal-tab::leaf-1',
@@ -153,4 +160,26 @@ describe('structured session cold restoration', () => {
])
expect(closed.tabGroups?.[0]?.tabOrder).toEqual(['terminal-tab'])
})
it('commits the host close when the renderer already removed the structured tab', async () => {
const runtime = new OrcaRuntimeService()
runtime.setNotifier({
closeSessionTab: vi.fn(async () => {
throw new Error('session_tab_not_found')
})
} as never)
runtime.publishStructuredAgentSessionTab({
workspaceId: 'workspace-1',
sessionId: 'session-1',
agent: 'codex',
activate: true
})
await runtime.closeMobileSessionTab('id:workspace-1', 'agent-session:session-1', {
reason: 'user'
})
const snapshot = await runtime.listMobileSessionTabs('id:workspace-1')
expect(snapshot.tabs).toEqual([])
})
})
@@ -76,7 +76,10 @@ function installProofPty(
ptysById: Map<string, { tailBuffer: string[]; lastOutputAt: number }>
}
const pty = record.ptysById.get(PTY_ID)!
pty.tailBuffer = [`Session ID: ${threadId}`]
pty.tailBuffer = [
'\u001b[2m│ >_ OpenAI Codex (v0.148.0) │',
`│ Session: \u001b[22m${threadId}\u001b[2m │`
]
pty.lastOutputAt = ptyRecord.lastOutputAt + 1_000
}
return true
@@ -237,6 +240,21 @@ describe('structured Codex adoption against the real PTY write gate', () => {
})
}, 20_000)
it('adopts a blank Codex 0.148 session before the lazy rollout exists', async () => {
await rm(join(rig.codexHome, 'sessions'), { recursive: true, force: true })
const result = await rig.runtime.adoptStructuredAgentSessionTerminal(adoptInput(), {
callerKey: 'renderer-1'
})
expect(result).toMatchObject({ ok: true })
expect(rig.refusedWrites).toEqual([])
expect(rig.store.getRecord(SESSION_ID)).toMatchObject({
providerHandleChain: [{ origin: 'adopted', handle: { threadId: THREAD_ID } }],
lease: { runtimeKind: 'tui', claimStatus: 'live' }
})
}, 20_000)
it('leaves nothing latched when the proof fails, so the next attempt still succeeds', async () => {
await rm(join(rig.codexHome, 'sessions'), { recursive: true, force: true })
+16 -3
View File
@@ -94,6 +94,7 @@ import {
type StructuredTuiOwner
} from '../native-chat/agent-session-wire/structured-agent-session-handoff-types'
import type { AgentSessionRecord } from '../../shared/agent-session-record'
import { SESSION_TAB_NOT_FOUND_ERROR } from '../../shared/session-tab-close'
import {
agentSessionOwnerBindingsEqual,
cloneAgentSessionOwnerBinding,
@@ -9471,7 +9472,19 @@ export class OrcaRuntimeService {
// snapshot so paired clients stop showing it.
await this.closeHeadlessMobileBrowserTab(worktreeId, snapshot, tab)
} else if (tab.type === 'agent-session') {
this.notifier?.closeSessionTab?.(tab.id, worktreeId)
if (this.notifier?.closeSessionTab) {
try {
await this.notifier.closeSessionTab(
structuredAgentSessionTabId(tab.sessionId),
worktreeId
)
} catch (error) {
// The renderer already having removed the tab is an idempotent close, not a veto.
if (!(error instanceof Error && error.message === SESSION_TAB_NOT_FOUND_ERROR)) {
throw error
}
}
}
this.closeStructuredAgentSessionTab(worktreeId, snapshot, tab)
} else {
if (!this.notifier?.closeSessionTab) {
@@ -11616,7 +11629,7 @@ export class OrcaRuntimeService {
spawnToken
}) ?? false
}
let transcriptPath: string
let transcriptPath: string | undefined
if (threadId) {
const proof = await proveCodexTuiRollout({ ...proofInput, threadId })
transcriptPath = proof.transcriptPath
@@ -11651,7 +11664,7 @@ export class OrcaRuntimeService {
fence,
observedAt: Date.now()
}),
transcriptPath,
...(transcriptPath ? { transcriptPath } : {}),
historySource: 'provider-resume',
adoptedTerminal: true
}
@@ -159,6 +159,31 @@ describe('a client that disappears without cleanup', () => {
expect(closeSession).not.toHaveBeenCalled()
expect(host.hasSession(SESSION)).toBe(true)
})
it('releases a desktop subscription when its renderer transport dies', async () => {
const transport = new AbortController()
await dispatcher.dispatchStreaming(
{
id: 'desktop-subscription',
authToken: 'token',
method: 'agentSession.subscribe',
params: { sessionId: SESSION }
},
() => {},
{
signal: transport.signal,
clientId: 'desktop-renderer',
clientKind: 'runtime',
clientCapabilities: CLIENT.clientCapabilities
}
)
expect(host.isHeld(SESSION)).toBe(true)
transport.abort()
await vi.waitFor(() => expect(host.hasSession(SESSION)).toBe(false))
expect(closeSession).toHaveBeenCalledWith(SESSION)
})
})
// Paired builds ship on their own schedule, and the ones already in the field never learned to
@@ -252,15 +252,23 @@ export const STRUCTURED_AGENT_SESSION_METHODS: RpcAnyMethod[] = [
)
let closed = false
let dispose = (): void => {}
ctx.runtime.registerSubscriptionCleanup(
let releaseTransportSubscription = (): void => {}
const onTransportAbort = (): void => releaseTransportSubscription()
const registration = ctx.runtime.registerOwnedSubscriptionCleanup(
subscriptionId,
() => {
closed = true
ctx.signal?.removeEventListener('abort', onTransportAbort)
dispose()
host.release(params.sessionId, streamHolder)
},
ctx.connectionId
)
releaseTransportSubscription = registration.releaseIfCurrent
ctx.signal?.addEventListener('abort', onTransportAbort, { once: true })
if (ctx.signal?.aborted) {
onTransportAbort()
}
if (closed) {
return
}
@@ -1,4 +1,5 @@
import { useCallback } from 'react'
import { toast } from 'sonner'
import type { Tab } from '../../../../shared/tab-types'
import { useAppStore } from '../../store'
import { destroyWorkspaceWebviews } from '../../store/slices/browser-webview-cleanup'
@@ -12,6 +13,17 @@ import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner
import { browserWorkspaceHasRemoteOwner } from '@/runtime/remote-browser-tab-ownership'
import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
import { toRuntimeWorktreeSelector } from '@/runtime/runtime-worktree-selector'
import { translate } from '@/i18n/i18n'
function reportStructuredSessionCloseError(error: unknown): void {
toast.error(
translate(
'components.native-chat.structuredSessionCloseFailed',
'Could not close this Codex chat'
),
{ description: error instanceof Error ? error.message : String(error) }
)
}
export function useTabGroupTabCloseCommands({
worktreeId,
@@ -87,10 +99,13 @@ export function useTabGroupTabCloseCommands({
tabId: `agent-session:${item.entityId}`,
reason: 'user'
})
closeUnifiedTab(item.id)
if (!opts?.skipEmptyCheck) {
leaveWorktreeIfEmpty()
}
.then(() => {
closeUnifiedTab(item.id)
if (!opts?.skipEmptyCheck) {
leaveWorktreeIfEmpty()
}
})
.catch(reportStructuredSessionCloseError)
return
}
if (item.contentType === 'terminal') {
@@ -164,7 +179,8 @@ export function useTabGroupTabCloseCommands({
tabId: `agent-session:${item.entityId}`,
reason: 'user'
})
closeUnifiedTab(item.id)
.then(() => closeUnifiedTab(item.id))
.catch(reportStructuredSessionCloseError)
continue
}
if (item.contentType === 'terminal' && isWebRuntimeSessionActive(runtimeEnvironmentId)) {
+3 -1
View File
@@ -16113,7 +16113,9 @@
"orchestrationPaused": {
"label": "Orchestration paused",
"message": "Structured Chat blocks terminal prompts and sends. Orchestration messages remain queued; switch to Terminal, then check the Orca inbox with"
}
},
"structuredSessionCloseFailed": "Could not close this Codex chat",
"structuredSessionLaunchFailed": "Could not open Codex chat"
},
"tab": {
"bar": {
@@ -1,3 +1,4 @@
import { toast } from 'sonner'
import { useAppStore } from '@/store'
import type { AgentStartupPlan } from '@/lib/tui-agent-startup'
import { planLaunchAgentStartupPrompt } from '@/lib/launch-agent-startup-prompt-plan'
@@ -31,6 +32,8 @@ import { resolveInitialNativeChatSessionOptions } from '@/components/native-chat
import { seedNativeChatAppliedSessionOptions } from '@/components/native-chat/native-chat-session-option-cache'
import { canUseStructuredNativeChat } from '@/lib/structured-native-chat-availability'
import { waitForAgentReady } from '@/lib/agent-ready-wait'
import { launchStructuredCodexSession } from '@/lib/launch-structured-codex-session'
import { translate } from '@/i18n/i18n'
export type LaunchAgentInNewTabArgs = {
agent: TuiAgent
@@ -176,6 +179,26 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI
}
}
const launchDirectStructuredChat =
agent === 'codex' &&
!hasPrompt &&
initialViewModeProps.viewMode === 'chat' &&
canUseStructuredNativeChat(store, worktreeId)
if (launchDirectStructuredChat) {
void launchStructuredCodexSession(worktreeId).catch((error) => {
toast.error(
translate(
'components.native-chat.structuredSessionLaunchFailed',
'Could not open Codex chat'
),
{
description: error instanceof Error ? error.message : String(error)
}
)
})
return { tabId: null, startupPlan, pasteDraftAfterLaunch: false }
}
// Why: queue startup BEFORE TerminalPane mounts — it snapshots pendingStartupByTabId in useState on first render.
// Why: followup path pastes an unsubmitted draft, so gate the initial chat view like a draft launch, not auto-submit.
const adoptStructuredChat =
@@ -5,6 +5,7 @@ const mockSetTabViewMode = vi.fn()
const mockWaitForAgentReady = vi.fn()
const mockPasteDraftWhenAgentReady = vi.fn()
const mockMarkNativeChatLaunchPromptFailed = vi.fn()
const mockLaunchStructuredCodexSession = vi.fn()
const store = {
activeRepoId: 'repo-1',
@@ -61,6 +62,9 @@ vi.mock('@/runtime/web-runtime-session', () => ({
isWebRuntimeSessionActive: vi.fn(() => false),
isWebTerminalSurfaceTabId: vi.fn(() => false)
}))
vi.mock('@/lib/launch-structured-codex-session', () => ({
launchStructuredCodexSession: mockLaunchStructuredCodexSession
}))
/** Structured adoption creates the tab in terminal mode and flips it to chat once
* Codex is ready; the bridge stamps `viewMode: 'chat'` on the tab up front. That
@@ -73,24 +77,25 @@ describe('structured chat adoption guard on the launch path', () => {
mockCreateTab.mockReturnValue({ id: 'tab-1' })
mockWaitForAgentReady.mockResolvedValue({ ready: true, reason: 'foreground-match' })
mockPasteDraftWhenAgentReady.mockResolvedValue(true)
mockLaunchStructuredCodexSession.mockResolvedValue('codex-session-1')
})
it('adopts a local Codex tab into the structured stack', async () => {
it('honors the persisted chat default by launching Codex directly as a structured session', async () => {
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
launchAgentInNewTab({ agent: 'codex', worktreeId: 'wt-1' })
const result = launchAgentInNewTab({ agent: 'codex', worktreeId: 'wt-1' })
expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, {
launchAgent: 'codex'
})
await vi.waitFor(() => expect(mockSetTabViewMode).toHaveBeenCalledWith('tab-1', 'chat'))
expect(result).toMatchObject({ tabId: null, pasteDraftAfterLaunch: false })
expect(mockLaunchStructuredCodexSession).toHaveBeenCalledWith('wt-1')
expect(mockCreateTab).not.toHaveBeenCalled()
expect(mockWaitForAgentReady).not.toHaveBeenCalled()
})
it('stays in terminal view when Codex never became ready', async () => {
mockWaitForAgentReady.mockResolvedValue({ ready: false, reason: 'timeout' })
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
launchAgentInNewTab({ agent: 'codex', worktreeId: 'wt-1' })
launchAgentInNewTab({ agent: 'codex', worktreeId: 'wt-1', prompt: 'start this task' })
await vi.waitFor(() => expect(mockWaitForAgentReady).toHaveBeenCalled())
// Let every continuation the ready wait could have queued run before asserting it did not flip.
@@ -0,0 +1,50 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { structuredAgentSessionPayloadFingerprint } from '../../../shared/structured-agent-session-mutation'
import { callStructuredAgentSession } from '@/runtime/structured-agent-session-client'
import { launchStructuredCodexSession } from './launch-structured-codex-session'
vi.mock('@/runtime/structured-agent-session-client', () => ({
callStructuredAgentSession: vi.fn()
}))
describe('structured Codex launch', () => {
beforeEach(() => {
vi.mocked(callStructuredAgentSession).mockReset()
})
it('creates a native session with a host-verifiable launch intent', async () => {
vi.mocked(callStructuredAgentSession).mockImplementation(async (_target, _method, params) => ({
ok: true,
replayed: false,
fence: 1,
cursor: { epoch: 'epoch-1', sequence: 0 },
value: {
sessionId: (params as { envelope: { sessionId: string } }).envelope.sessionId,
fence: 1,
snapshot: { cursor: { epoch: 'epoch-1', sequence: 0 }, items: [] },
unconfirmedClientMessageIds: []
}
}))
const sessionId = await launchStructuredCodexSession('workspace-1')
const params = vi.mocked(callStructuredAgentSession).mock.calls[0]?.[2] as {
envelope: { sessionId: string; payloadFingerprint: string }
worktree: string
agent: 'codex'
}
expect(sessionId).toMatch(/^codex_[A-Za-z0-9_]{36}$/)
expect(callStructuredAgentSession).toHaveBeenCalledWith(
{ kind: 'local' },
'agentSession.create',
expect.objectContaining({ worktree: 'id:workspace-1', agent: 'codex' })
)
expect(params.envelope.payloadFingerprint).toBe(
structuredAgentSessionPayloadFingerprint({
method: 'agentSession.create',
sessionId: params.envelope.sessionId,
fields: { worktree: 'id:workspace-1', agent: 'codex' }
})
)
})
})
@@ -0,0 +1,38 @@
import type {
AgentSessionAttachResult,
AgentSessionMutationResult
} from '../../../shared/agent-session-wire'
import {
createStructuredAgentSessionOperationId,
structuredAgentSessionPayloadFingerprint
} from '../../../shared/structured-agent-session-mutation'
import { callStructuredAgentSession } from '@/runtime/structured-agent-session-client'
import { toRuntimeWorktreeSelector } from '@/runtime/runtime-worktree-selector'
function newSessionId(): string {
return `codex_${crypto.randomUUID().replaceAll('-', '_')}`
}
export async function launchStructuredCodexSession(worktreeId: string): Promise<string> {
const sessionId = newSessionId()
const fields = { worktree: toRuntimeWorktreeSelector(worktreeId), agent: 'codex' as const }
const result = await callStructuredAgentSession<
AgentSessionMutationResult<AgentSessionAttachResult>
>({ kind: 'local' }, 'agentSession.create', {
envelope: {
sessionId,
clientOperationId: createStructuredAgentSessionOperationId(() => crypto.randomUUID()),
expectedRuntimeFence: null,
payloadFingerprint: structuredAgentSessionPayloadFingerprint({
method: 'agentSession.create',
sessionId,
fields
})
},
...fields
})
if (!result.ok) {
throw new Error(result.refusal.message)
}
return result.value.sessionId
}