Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
baa42c8
feat(native-chat): release a structured session when its chat closes
brennanb2025 Aug 19, 2026
5da53f1
fix(native-chat): stop eviction losing the rows a closing provider emits
brennanb2025 Aug 19, 2026
1b8301e
feat(native-chat): give a structured session a surface-holder lifetime
brennanb2025 Aug 19, 2026
d98df97
Merge structured-tui-adoption into structured-session-eviction
brennanb2025 Aug 20, 2026
efda0af
Merge origin/main into structured-session-eviction
brennanb2025 Aug 20, 2026
e473bc5
fix(native-chat): reserve the lease before the adopted Codex proof
brennanb2025 Aug 20, 2026
ca409f0
fix(native-chat): let the reservation holder win an overlapping adoption
brennanb2025 Aug 20, 2026
e0aeecd
fix(native-chat): adopt a Codex pane on its own account home
brennanb2025 Aug 20, 2026
3d539b7
Merge branch 'brennanb2025/structured-tui-adoption' into brennanb2025…
brennanb2025 Aug 23, 2026
f4058e3
Merge branch 'brennanb2025/structured-tui-adoption' into brennanb2025…
brennanb2025 Aug 23, 2026
c103bc3
fix(orchestration): surface structured chat pauses
brennanb2025 Aug 23, 2026
abb0f3e
Merge remote-tracking branch 'origin/brennanb2025/structured-tui-adop…
brennanb2025 Aug 23, 2026
8572614
Merge remote-tracking branch 'origin/brennanb2025/structured-tui-adop…
brennanb2025 Aug 23, 2026
c9f4c51
perf: batch structured session lease renewal
brennanb2025 Aug 23, 2026
20e77f6
fix structured session restart overlap
brennanb2025 Aug 23, 2026
17d447e
wip(native-chat): holder release and rollout proof edits from stalled…
brennanb2025 Aug 23, 2026
f941a23
fix(native-chat): finish holder and close cleanup
brennanb2025 Aug 23, 2026
330e4d8
Merge remote-tracking branch 'origin/brennanb2025/structured-tui-adop…
brennanb2025 Aug 23, 2026
ccff8bb
fix(cli): fail refused terminal sends truthfully
brennanb2025 Aug 23, 2026
631803a
fix(native-chat): await codex descendant teardown
brennanb2025 Aug 24, 2026
3037788
Merge remote-tracking branch 'origin/brennanb2025/structured-holder-a…
brennanb2025 Aug 24, 2026
0ed7f33
Merge remote-tracking branch 'origin/brennanb2025/structured-restart-…
brennanb2025 Aug 24, 2026
f3fb7b8
Merge remote-tracking branch 'origin/brennanb2025/structured-linux-te…
brennanb2025 Aug 24, 2026
8317d49
Merge remote-tracking branch 'origin/brennanb2025/structured-refusal-…
brennanb2025 Aug 24, 2026
849b83f
fix(runtime): preserve recovered store and cmd launch paths
brennanb2025 Aug 24, 2026
314ac13
Merge remote-tracking branch 'origin/brennanb2025/structured-win-stor…
brennanb2025 Aug 24, 2026
e763eb0
Merge structured-lease-renewal-scale into structured-session-eviction
brennanb2025 Aug 24, 2026
3c112d0
chore: remove stray root-level scale report that leaks CI static-anal…
brennanb2025 Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions config/scripts/structured-lease-renewal-benchmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { performance } from 'node:perf_hooks'
import { AgentSessionRecordStore } from '../../src/main/runtime/agent-session-record-store'
import { readProcessStartTimesMs } from '../../src/main/runtime/agent-session-process-identity-probe'
import { StructuredAgentSessionLeaseRenewer } from '../../src/main/native-chat/agent-session-wire/structured-agent-session-lease-renewer'

const RECORD_COUNT = 50
const NOW = 1_800_000_000_000

async function seedStore(store: AgentSessionRecordStore, directory: string): Promise<void> {
for (let index = 0; index < RECORD_COUNT; index += 1) {
const sessionId = `benchmark-session-${index}`
const spawnToken = `benchmark-spawn-${index}`
const reserved = await store.reserveOwner({
sessionId,
location: {
executionHostId: 'local',
wslDistro: null,
workspaceId: 'benchmark-workspace',
workspaceKind: 'folder'
},
provider: 'codex',
accountHome: { variable: 'CODEX_HOME', path: directory },
runtimeKind: 'native',
expectedFence: null,
spawnToken,
claimKeyId: 'benchmark-key',
handoffOperationId: null,
probe: { outcome: 'reservation-unused' },
operation: {
callerKey: 'benchmark',
operationId: `${NOW}-${index.toString(16).padStart(32, '0')}`,
fingerprint: `create-${index}`
},
now: NOW
})
const fence = reserved.record.lease.runtimeFence
await store.commitProcessIdentity({
sessionId,
fence,
process: {
hostId: 'local',
pid: process.pid,
processStartTimeMs: null,
spawnToken
},
now: NOW
})
await store.proveOwner({
sessionId,
fence,
link: {
linkId: `benchmark-link-${index}`,
handle: { provider: 'codex', threadId: `benchmark-thread-${index}` },
origin: 'created',
mintedAtFence: fence,
observedAt: NOW
},
now: NOW
})
}
}

async function main(): Promise<void> {
const directory = await mkdtemp(join(tmpdir(), 'orca-lease-renewal-benchmark-'))
try {
const store = await AgentSessionRecordStore.open({ directory, hostId: 'local' })
await seedStore(store, directory)
const renewer = new StructuredAgentSessionLeaseRenewer({
store,
probe: async () => ({ outcome: 'identity-matched', matchedOn: ['spawn-token'] }),
now: () => NOW + 10_000
})
const startedAt = performance.now()
await renewer.renewNow()
const elapsedMs = performance.now() - startedAt
const renewed = store
.listRecords()
.filter((record) => record.lease.lastRenewedAt === NOW + 10_000).length
const processProbeStartedAt = performance.now()
const processStartTimes = await readProcessStartTimesMs(
Array.from({ length: RECORD_COUNT }, () => process.pid),
'darwin'
)
console.log(
JSON.stringify({
records: RECORD_COUNT,
renewed,
elapsedMs,
processTableElapsedMs: performance.now() - processProbeStartedAt,
processTableMatches:
processStartTimes.get(process.pid) === null ||
processStartTimes.get(process.pid) === undefined
? 0
: RECORD_COUNT
})
)
} finally {
await rm(directory, { recursive: true, force: true })
}
}

void main().catch((error: unknown) => {
console.error(error)
process.exitCode = 1
})
63 changes: 63 additions & 0 deletions mobile/src/session/mobile-structured-session-hold.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { RpcClient } from '../transport/rpc-client'
import { useMobileStructuredSessionHold } from './mobile-structured-session-hold'

function renderHold(props: { client: RpcClient | null; sessionId: string | null }): {
renderer: ReactTestRenderer
} {
function Probe(): null {
useMobileStructuredSessionHold(props)
return null
}
let renderer!: ReactTestRenderer
act(() => {
renderer = create(createElement(Probe))
})
return { renderer }
}

function client(sendRequest: ReturnType<typeof vi.fn>): RpcClient {
return { sendRequest } as unknown as RpcClient
}

let rendered: ReactTestRenderer | null = null

afterEach(() => {
act(() => rendered?.unmount())
rendered = null
})

describe('a paired session view', () => {
it('holds the session while it is open and releases it when it closes', async () => {
const sendRequest = vi.fn(async () => ({ ok: true, result: {} }))
const { renderer } = renderHold({ client: client(sendRequest), sessionId: 'session-alpha' })

const hold = sendRequest.mock.calls.find(([method]) => method === 'agentSession.hold')
expect(hold?.[1]).toMatchObject({ sessionId: 'session-alpha' })

await act(async () => renderer.unmount())

const release = sendRequest.mock.calls.find(([method]) => method === 'agentSession.release')
expect(release?.[1]).toEqual(hold?.[1])
})

it('asks for nothing before there is a session to hold', () => {
const sendRequest = vi.fn(async () => ({ ok: true, result: {} }))
rendered = renderHold({ client: client(sendRequest), sessionId: null }).renderer

expect(sendRequest).not.toHaveBeenCalled()
})

// An older host answers method_not_found; a chat that cannot be held is still a chat that reads.
it('survives a host that does not know the method', () => {
const sendRequest = vi.fn(async () => {
throw new Error('method_not_found')
})

expect(() => {
rendered = renderHold({ client: client(sendRequest), sessionId: 'session-alpha' }).renderer
}).not.toThrow()
})
})
37 changes: 37 additions & 0 deletions mobile/src/session/mobile-structured-session-hold.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// The paired session view telling the host it is looking at this chat.
//
// Same contract as the desktop chat's hold, over the mobile transport. It matters more here: a
// phone is the surface most likely to disappear without saying anything, so the host's own
// connection-close backstop is what usually ends up releasing this — the explicit release only
// covers the polite case of navigating away with the socket still up.
//
// A host that predates the method answers `method_not_found`; the session still reads.

import { useEffect } from 'react'
import type { RpcClient } from '../transport/rpc-client'

let holderOrdinal = 0

export function useMobileStructuredSessionHold(args: {
client: RpcClient | null
sessionId: string | null
}): void {
const { client, sessionId } = args
useEffect(() => {
if (!client || !sessionId) {
return
}
holderOrdinal += 1
const holderId = `mobile-session:${holderOrdinal}`
// Chained, not raced: unmounting during the hold's round trip would otherwise release a hold
// that has not been taken yet.
const held = client
.sendRequest('agentSession.hold', { sessionId, holderId })
.catch(() => undefined)
return () => {
void held.then(() =>
client.sendRequest('agentSession.release', { sessionId, holderId }).catch(() => undefined)
)
}
}, [client, sessionId])
}
3 changes: 3 additions & 0 deletions mobile/src/session/use-mobile-structured-agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type StructuredAgentSessionState
} from '../../../src/shared/structured-agent-session-reducer'
import type { RpcClient } from '../transport/rpc-client'
import { useMobileStructuredSessionHold } from './mobile-structured-session-hold'
import {
createMobileStructuredReconnectState,
noteStructuredBackground,
Expand Down Expand Up @@ -58,6 +59,8 @@ export function useMobileStructuredAgentSession(args: {
loadOlder: () => Promise<boolean>
} {
const { client, sessionId } = args
// Declared before the read: the hold is what gives a restored session its provider child back.
useMobileStructuredSessionHold({ client, sessionId })
const [state, dispatch] = useReducer(reduceStructuredAgentSession, EMPTY_STRUCTURED_AGENT_SESSION)
const stateRef = useRef(state)
const [loadingOlder, setLoadingOlder] = useState(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ describe('mobile E2EE v2 physical channel', () => {
expect(JSON.parse(new TextDecoder().decode(capabilities!))).toEqual({
type: 'e2ee_client_capabilities',
v: 1,
clientCapabilities: ['agent-session.structured.v1']
clientCapabilities: ['agent-session.structured.v1', 'agent-session.structured.hold.v1']
})
expect(ctx.onAuthenticated).toHaveBeenCalledOnce()
expect(ctx.onError).not.toHaveBeenCalled()
Expand Down
10 changes: 8 additions & 2 deletions mobile/src/transport/mobile-e2ee-v2-physical-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import {
createWsOutboundBackpressureQueue,
type WsOutboundBackpressureQueue
} from '../../../src/shared/ws-outbound-backpressure-queue'
import { STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY } from '../../../src/shared/protocol-version'
import {
STRUCTURED_AGENT_SESSION_HOLD_RUNTIME_CAPABILITY,
STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY
} from '../../../src/shared/protocol-version'
import type { MobileE2EEV2ClientSession } from './mobile-e2ee-v2-client-session'

type ChannelState = 'awaiting-ready' | 'awaiting-authenticated' | 'ready'
Expand Down Expand Up @@ -117,7 +120,10 @@ export class MobileE2EEV2PhysicalChannel {
plaintext: JSON.stringify({
type: 'e2ee_client_capabilities',
v: 1,
clientCapabilities: [STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY]
clientCapabilities: [
STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY,
STRUCTURED_AGENT_SESSION_HOLD_RUNTIME_CAPABILITY
]
})
})
this.args.onAuthenticated()
Expand Down
4 changes: 4 additions & 0 deletions mobile/src/transport/rpc-client-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export type RpcClient = {
getState: () => ConnectionState
getReconnectAttempt: () => number
getLastConnectedAt: () => number | null
// Wall-clock stamp of the last inbound frame on the current session, or null when the
// transport can't vouch for one. Optional so older/foreign RpcClient shapes stay valid;
// callers must treat absent/null as "unknown" and fall back to a safe bound.
getLastInboundAt?: () => number | null
onStateChange: (listener: (state: ConnectionState) => void) => () => void
notifyForeground: (reason?: ForegroundNudgeReason) => void
restartAfterStructuredBackground?: () => void
Expand Down
2 changes: 1 addition & 1 deletion mobile/src/transport/rpc-client-structured-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ describe('structured session RPC transport', () => {
expect(auth).toEqual({
type: 'e2ee_auth',
deviceToken: 'token',
clientCapabilities: ['agent-session.structured.v1']
clientCapabilities: ['agent-session.structured.v1', 'agent-session.structured.hold.v1']
})
client.close()
})
Expand Down
10 changes: 8 additions & 2 deletions mobile/src/transport/rpc-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ import {
} from './rpc-session-liveness-watchdog'
import { isStaleForegroundDial } from './rpc-stale-dial'
import { websocketPayloadToUint8 } from './websocket-payload-bytes'
import { STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY } from '../../../src/shared/protocol-version'
import {
STRUCTURED_AGENT_SESSION_HOLD_RUNTIME_CAPABILITY,
STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY
} from '../../../src/shared/protocol-version'
import type { RpcClient, SendRequestOptions, SubscribeOptions } from './rpc-client-contract'

export type { RpcClient, SendRequestOptions } from './rpc-client-contract'
Expand Down Expand Up @@ -374,7 +377,10 @@ export function connect(
sendEncrypted({
type: 'e2ee_auth',
deviceToken,
clientCapabilities: [STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY]
clientCapabilities: [
STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY,
STRUCTURED_AGENT_SESSION_HOLD_RUNTIME_CAPABILITY
]
})
return
}
Expand Down
46 changes: 46 additions & 0 deletions src/cli/handlers/orchestration-worker-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,52 @@ describe('orchestration worker-start CLI contract', () => {
expect(process.exitCode).toBe(1)
})

it('prints the Structured Chat recovery action for a refused worker start', async () => {
callMock.mockResolvedValue({
result: {
taskId: 'task_1',
dispatchId: 'ctx_1',
state: 'failed',
failedStage: 'dispatch_input',
lastError:
'The target terminal is in Structured Chat. Switch it to Terminal, then retry `orca orchestration worker-start`.',
effects: [],
residualResources: []
}
})

await ORCHESTRATION_HANDLERS['orchestration worker-start']({
flags: new Map<string, string | boolean>([
['task', 'task_1'],
['terminal', 'term_worker'],
['from', 'term_coord']
]),
client: { call: callMock },
cwd: '/tmp/repo',
json: false
} as never)

const formatter = vi.mocked(printResult).mock.calls[0]?.[2] as
| ((result: {
taskId: string
dispatchId: string
state: string
failedStage?: string
lastError?: string
}) => string)
| undefined
expect(
formatter?.({
taskId: 'task_1',
dispatchId: 'ctx_1',
state: 'failed',
failedStage: 'dispatch_input',
lastError:
'The target terminal is in Structured Chat. Switch it to Terminal, then retry `orca orchestration worker-start`.'
})
).toMatch(/Structured Chat.*Switch it to Terminal.*orca orchestration worker-start/s)
})

it('prints a reveal warning for a live background worker', async () => {
callMock.mockResolvedValue({
result: {
Expand Down
Loading
Loading