rm git shim: neutralize stale wrappers without a host gate (#14255)

* Revert "fix terminal attribution shim removal edge cases (#14187)"

This reverts 585dd6d3a9. Re-landed in the next commit without the host capability gate. Nothing shipped with it, so no migration constraint.

* rm git shim: neutralize stale wrappers without a host gate

Re-lands the cleanup half of #14187: pass-through tombstones for retained wrapper paths, env/PATH scrubbing at every spawn owner, and the retired setting drop.

Only writes tombstones when the legacy directory already exists, so a clean install no longer has it created. Leaves out the terminal.attribution-removed.v1 capability gate: the tombstone neutralizes each host locally, so refusing terminal create/split against older hosts denied service without adding cleanup.

* rm git shim: surface neutralization failures and fix rollback marker

Readiness review follow-ups: warn on each failed attempt and on give-up (was silent and undiagnosable); write a VERSION marker distinct from the retired shim's '7' so a rolled-back build rewrites its own wrappers; clear a captured ORCA_REAL_* path that no longer exists so the cmd wrapper's where.exe fallback can run; stop a locked temp file masking the real error. Adds retry-exhaustion coverage.

* rm git shim: pin the cmd fallback order and correct the give-up count

Round-2 review follow-ups: string-pin that a stale ORCA_REAL_* is cleared before the where.exe fallback, and count the initial attempt in the give-up warning so it agrees with the per-attempt line.

* rm git shim: keep the split-failure toast

The revert took a toast that #14187 added alongside the gate but which stands on its own: without it a failed remote split only reaches the console and the pane silently never appears. Also pins attempt ordinals in the retry-exhaustion test.
This commit is contained in:
Neil
2026-08-13 03:01:45 -07:00
committed by GitHub
parent 4c5f818187
commit 4882eeb8ac
66 changed files with 555 additions and 2199 deletions
+17 -40
View File
@@ -127,15 +127,6 @@ import type { TerminalLiveInputSender } from '../../../../src/terminal/terminal-
import { isTerminalSendRpcAccepted } from '../../../../src/terminal/terminal-send-rpc-response'
import { sendMobileTerminalQueryReply } from '../../../../src/terminal/mobile-terminal-query-reply'
import { TERMINAL_QUERY_REPLY_INPUT_RUNTIME_CAPABILITY } from '../../../../../src/shared/protocol-version'
import {
addLegacyTerminalAttributionDisableRequest,
MOBILE_TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE,
withLegacyTerminalAttributionDisabledEnv
} from '../../../../../src/shared/legacy-terminal-attribution-env'
import {
assertMobileTerminalAttributionDisableSupported,
MOBILE_TERMINAL_CREATE_RPC_OPTIONS
} from '../../../../src/session/mobile-terminal-attribution-compat'
import { useTerminalLiveInputCommit } from '../../../../src/terminal/use-terminal-live-input-commit'
import { resolveMobileTerminalInputGate } from '../../../../src/terminal/terminal-input-connection-gate'
import {
@@ -3674,27 +3665,20 @@ export default function SessionScreen() {
.slice(2, 10)}`
try {
const authority = await assertMobileTerminalAttributionDisableSupported(client)
const response = await client.sendRequest(
'session.tabs.createTerminal',
{
worktree: `id:${worktreeId}`,
afterTabId: activeSessionTabId ?? undefined,
clientMutationId,
...(options?.startupCommand ? { command: options.startupCommand } : {}),
...(options?.startupCommandDelivery
? { startupCommandDelivery: options.startupCommandDelivery }
: {}),
env: withLegacyTerminalAttributionDisabledEnv(undefined),
envToDelete: addLegacyTerminalAttributionDisableRequest(undefined),
...(options?.agentPrompt ? { agentPrompt: options.agentPrompt } : {}),
...(agent ? { agent } : {}),
activate: false,
select: true,
navigation: 'caller'
},
{ ...MOBILE_TERMINAL_CREATE_RPC_OPTIONS, expectedRuntimeId: authority.runtimeId }
)
const response = await client.sendRequest('session.tabs.createTerminal', {
worktree: `id:${worktreeId}`,
afterTabId: activeSessionTabId ?? undefined,
clientMutationId,
...(options?.startupCommand ? { command: options.startupCommand } : {}),
...(options?.startupCommandDelivery
? { startupCommandDelivery: options.startupCommandDelivery }
: {}),
...(options?.agentPrompt ? { agentPrompt: options.agentPrompt } : {}),
...(agent ? { agent } : {}),
activate: false,
select: true,
navigation: 'caller'
})
if (response.ok) {
const result = (response as RpcSuccess).result as TerminalCreateResult
const created = result.tab
@@ -3794,17 +3778,10 @@ export default function SessionScreen() {
showToast(message, 1800)
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : null
const message =
errorMessage === MOBILE_TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE
? errorMessage
: (options?.errorToast ?? errorMessage ?? 'Failed to create terminal')
} catch {
const message = options?.errorToast ?? 'Failed to create terminal'
setCreateError(message)
if (
options?.errorToast ||
errorMessage === MOBILE_TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE
) {
if (options?.errorToast) {
triggerError()
showToast(message, 1800)
}
@@ -226,13 +226,6 @@ describe('resumeAiVaultSessionInTerminal', () => {
it('creates a fresh terminal and sends the command with Enter', async () => {
const sendRequest = vi
.fn()
.mockResolvedValueOnce({
ok: true,
result: {
runtimeId: 'runtime',
capabilities: ['terminal.attribution-removed.v1']
}
})
.mockResolvedValueOnce({
ok: true,
result: { tab: { type: 'terminal', id: 'tab-1', terminal: 'pty-1', title: 'Terminal' } }
@@ -256,20 +249,13 @@ describe('resumeAiVaultSessionInTerminal', () => {
navigation: 'caller'
})
).resolves.toMatchObject({ id: 'tab-1', terminal: 'pty-1' })
expect(sendRequest).toHaveBeenNthCalledWith(1, 'status.get', undefined, {
timeoutMs: 30_000,
budgetSpansConnect: true
})
expect(sendRequest).toHaveBeenNthCalledWith(
2,
1,
'session.tabs.createTerminal',
{
worktree: 'id:worktree-1',
env: {
ANTHROPIC_BASE_URL: 'http://localhost:3000',
ORCA_ATTRIBUTION_BYPASS: '1'
},
envToDelete: ['CODEX_HOME', 'ORCA_CODEX_HOME', 'ORCA_ENABLE_GIT_ATTRIBUTION'],
env: { ANTHROPIC_BASE_URL: 'http://localhost:3000' },
envToDelete: ['CODEX_HOME', 'ORCA_CODEX_HOME'],
launchConfig: {
agentCommand: 'claude',
agentArgs: '',
@@ -283,14 +269,10 @@ describe('resumeAiVaultSessionInTerminal', () => {
},
// Why: a socket drop mid-resume must reject within the request timeout
// instead of parking on the reconnect waiter with the spinner pinned.
{
timeoutMs: RESUME_RPC_TIMEOUT_MS,
budgetSpansConnect: true,
expectedRuntimeId: 'runtime'
}
{ timeoutMs: RESUME_RPC_TIMEOUT_MS }
)
expect(sendRequest).toHaveBeenNthCalledWith(
3,
2,
'terminal.send',
{
terminal: 'pty-1',
@@ -302,35 +284,17 @@ describe('resumeAiVaultSessionInTerminal', () => {
})
it('throws when terminal creation fails', async () => {
const sendRequest = vi
.fn()
.mockResolvedValueOnce({
ok: true,
result: {
runtimeId: 'runtime',
capabilities: ['terminal.attribution-removed.v1']
}
})
.mockResolvedValueOnce({
ok: false,
error: { message: 'no terminal' }
})
const sendRequest = vi.fn().mockResolvedValueOnce({
ok: false,
error: { message: 'no terminal' }
})
await expect(
resumeAiVaultSessionInTerminal({ sendRequest }, 'worktree-1', { command: 'command' })
).rejects.toThrow('no terminal')
})
it('throws when the created terminal response is malformed', async () => {
const sendRequest = vi
.fn()
.mockResolvedValueOnce({
ok: true,
result: {
runtimeId: 'runtime',
capabilities: ['terminal.attribution-removed.v1']
}
})
.mockResolvedValueOnce({ ok: true, result: { tab: { id: 'x' } } })
const sendRequest = vi.fn().mockResolvedValueOnce({ ok: true, result: { tab: { id: 'x' } } })
await expect(
resumeAiVaultSessionInTerminal({ sendRequest }, 'worktree-1', { command: 'command' })
).rejects.toThrow('Created terminal response was invalid')
@@ -339,13 +303,6 @@ describe('resumeAiVaultSessionInTerminal', () => {
it('throws when terminal send fails or is locked', async () => {
const failedSend = vi
.fn()
.mockResolvedValueOnce({
ok: true,
result: {
runtimeId: 'runtime',
capabilities: ['terminal.attribution-removed.v1']
}
})
.mockResolvedValueOnce({
ok: true,
result: { tab: { type: 'terminal', id: 'tab-1', terminal: 'pty-1' } }
@@ -359,13 +316,6 @@ describe('resumeAiVaultSessionInTerminal', () => {
const lockedSend = vi
.fn()
.mockResolvedValueOnce({
ok: true,
result: {
runtimeId: 'runtime',
capabilities: ['terminal.attribution-removed.v1']
}
})
.mockResolvedValueOnce({
ok: true,
result: { tab: { type: 'terminal', id: 'tab-1', terminal: 'pty-1' } }
@@ -377,17 +327,6 @@ describe('resumeAiVaultSessionInTerminal', () => {
})
).rejects.toThrow('Terminal input is locked')
})
it('refuses an old host before creating a resume terminal', async () => {
const sendRequest = vi
.fn()
.mockResolvedValueOnce({ ok: true, result: { appVersion: '1.4.89' } })
await expect(
resumeAiVaultSessionInTerminal({ sendRequest }, 'worktree-1', { command: 'command' })
).rejects.toThrow('Update the host and try again')
expect(sendRequest).toHaveBeenCalledTimes(1)
})
})
describe('createMobileAiVaultResumeMutationRegistry', () => {
+3 -13
View File
@@ -16,10 +16,6 @@ import { normalizeAiVaultResumeFilePath } from '../../../src/shared/ai-vault-res
import type { TuiAgent } from '../../../src/shared/types'
import { parseWslUncPath } from '../../../src/shared/wsl-paths'
import { resolveWindowsShellStartupFamily } from '../../../src/shared/windows-terminal-shell'
import {
addLegacyTerminalAttributionDisableRequest,
withLegacyTerminalAttributionDisabledEnv
} from '../../../src/shared/legacy-terminal-attribution-env'
import type { RpcClient } from '../transport/rpc-client'
import {
readMobileReviewCreatedTerminal,
@@ -27,7 +23,6 @@ import {
type MobileReviewTerminalTab
} from './mobile-diff-review-rpc'
import type { MobileAiVaultResumeTargetStatus } from '../agent-history/agent-history-resume-target'
import { assertMobileTerminalAttributionDisableSupported } from './mobile-terminal-attribution-compat'
const NODE_PLATFORMS = new Set<NodeJS.Platform>([
'aix',
@@ -174,13 +169,12 @@ export async function resumeAiVaultSessionInTerminal(
worktreeId: string,
launch: MobileAiVaultResumeLaunch & { clientMutationId?: string }
): Promise<MobileReviewTerminalTab> {
const authority = await assertMobileTerminalAttributionDisableSupported(client)
const created = await client.sendRequest(
'session.tabs.createTerminal',
{
worktree: `id:${worktreeId}`,
env: withLegacyTerminalAttributionDisabledEnv(launch.env),
envToDelete: addLegacyTerminalAttributionDisableRequest(launch.envToDelete),
...(launch.env ? { env: launch.env } : {}),
...(launch.envToDelete ? { envToDelete: launch.envToDelete } : {}),
...(launch.launchConfig ? { launchConfig: launch.launchConfig } : {}),
...(launch.launchAgent ? { launchAgent: launch.launchAgent } : {}),
...(launch.clientMutationId ? { clientMutationId: launch.clientMutationId } : {}),
@@ -188,11 +182,7 @@ export async function resumeAiVaultSessionInTerminal(
select: true,
navigation: 'caller'
},
{
timeoutMs: RESUME_RPC_TIMEOUT_MS,
budgetSpansConnect: true,
expectedRuntimeId: authority.runtimeId
}
{ timeoutMs: RESUME_RPC_TIMEOUT_MS }
)
if (!created.ok) {
throw new Error(created.error?.message || 'Failed to create terminal')
@@ -53,14 +53,6 @@ describe('prepareMobileAiVaultSessionResume', () => {
const sendRequest = vi
.fn()
.mockResolvedValueOnce({ ok: false, error })
.mockResolvedValueOnce({
ok: true,
result: {
runtimeId: 'runtime-1',
appVersion: '1.4.90',
capabilities: ['terminal.attribution-removed.v1']
}
})
.mockResolvedValueOnce({
ok: true,
result: { tab: { type: 'terminal', id: 'tab-1', terminal: 'pty-1', title: 'Terminal' } }
@@ -72,12 +64,9 @@ describe('prepareMobileAiVaultSessionResume', () => {
await resumeAiVaultSessionInTerminal({ sendRequest }, 'worktree-1', launch)
expect(prepared).toBe(legacy)
expect(sendRequest.mock.calls[2]?.[1]).toMatchObject({
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION']
})
expect(sendRequest.mock.calls[1]?.[1]).not.toHaveProperty('envToDelete')
expect(sendRequest).toHaveBeenNthCalledWith(
4,
3,
'terminal.send',
{
terminal: 'pty-1',
@@ -1,81 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { MOBILE_TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE } from '../../../src/shared/legacy-terminal-attribution-env'
import { assertMobileTerminalAttributionDisableSupported } from './mobile-terminal-attribution-compat'
function status(result: unknown) {
return {
id: 'status',
ok: true as const,
result: { runtimeId: 'runtime', ...(result as object) },
_meta: { runtimeId: 'runtime' }
}
}
describe('mobile terminal attribution compatibility', () => {
it('refuses hosts before terminal creation environment forwarding', async () => {
const sendRequest = vi.fn().mockResolvedValue(
status({
capabilities: [
'runtime.status.compat.v1',
'runtime.environments.v1',
'mobile.tasks.v1',
'workspace-run-context.v1'
]
})
)
await expect(assertMobileTerminalAttributionDisableSupported({ sendRequest })).rejects.toThrow(
MOBILE_TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE
)
expect(sendRequest).toHaveBeenCalledWith('status.get', undefined, {
timeoutMs: 30_000,
budgetSpansConnect: true
})
})
it('rejects historical hosts without explicit removal capability', async () => {
const sendRequest = vi.fn().mockResolvedValue(status({ appVersion: '1.4.90' }))
await expect(assertMobileTerminalAttributionDisableSupported({ sendRequest })).rejects.toThrow(
MOBILE_TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE
)
})
it('accepts capability-proven current development hosts', async () => {
const sendRequest = vi
.fn()
.mockResolvedValue(
status({ appVersion: '0.0.0-dev', capabilities: ['terminal.attribution-removed.v1'] })
)
await expect(assertMobileTerminalAttributionDisableSupported({ sendRequest })).resolves.toEqual(
{ runtimeId: 'runtime' }
)
})
it('fails closed when host status cannot be verified', async () => {
const sendRequest = vi.fn().mockResolvedValue({
id: 'status',
ok: false,
error: { code: 'offline', message: 'Host is offline' },
_meta: { runtimeId: 'runtime' }
})
await expect(assertMobileTerminalAttributionDisableSupported({ sendRequest })).rejects.toThrow(
'Host is offline'
)
})
it('fails closed with an actionable error for malformed host status', async () => {
const sendRequest = vi.fn().mockResolvedValue(
status({
appVersion: 1.49,
capabilities: 'terminal.attribution-removed.v1'
})
)
await expect(assertMobileTerminalAttributionDisableSupported({ sendRequest })).rejects.toThrow(
'Update the host and try again'
)
})
})
@@ -1,34 +0,0 @@
import {
hostSupportsSessionTabTerminalCreateAttributionDisable,
MOBILE_TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE
} from '../../../src/shared/legacy-terminal-attribution-env'
import type { RpcClient } from '../transport/rpc-client'
export const MOBILE_TERMINAL_CREATE_RPC_TIMEOUT_MS = 30_000
export const MOBILE_TERMINAL_CREATE_RPC_OPTIONS = {
timeoutMs: MOBILE_TERMINAL_CREATE_RPC_TIMEOUT_MS,
budgetSpansConnect: true
} as const
export type MobileTerminalAttributionAuthority = Readonly<{ runtimeId: string }>
export async function assertMobileTerminalAttributionDisableSupported(
client: Pick<RpcClient, 'sendRequest'>
): Promise<MobileTerminalAttributionAuthority> {
const response = await client.sendRequest(
'status.get',
undefined,
MOBILE_TERMINAL_CREATE_RPC_OPTIONS
)
if (!response.ok) {
throw new Error(response.error?.message || 'Unable to verify the workspace host version.')
}
if (!hostSupportsSessionTabTerminalCreateAttributionDisable(response.result)) {
throw new Error(MOBILE_TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE)
}
const runtimeId = Reflect.get(response.result as object, 'runtimeId')
if (typeof runtimeId !== 'string' || !runtimeId) {
throw new Error(MOBILE_TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE)
}
return Object.freeze({ runtimeId })
}
+13 -41
View File
@@ -12,10 +12,6 @@ function failure(message: string): RpcResponse {
const createdTerminal = success({ tab: { type: 'terminal', id: 't1', terminal: 'term-1' } })
const sendAccepted = success({ send: { accepted: true } })
const safeHost = success({
runtimeId: 'r',
capabilities: ['terminal.attribution-removed.v1']
})
function clientReturning(...responses: RpcResponse[]) {
const sendRequest = vi.fn(async () => responses[sendRequest.mock.calls.length - 1])
@@ -24,27 +20,16 @@ function clientReturning(...responses: RpcResponse[]) {
describe('createTerminalAndSendPrompt', () => {
it('creates a terminal then sends the prompt with enter', async () => {
const client = clientReturning(safeHost, createdTerminal, sendAccepted)
const client = clientReturning(createdTerminal, sendAccepted)
await createTerminalAndSendPrompt(client, 'wt-1', 'do the thing')
expect(client.sendRequest).toHaveBeenNthCalledWith(1, 'status.get', undefined, {
timeoutMs: 30_000,
budgetSpansConnect: true
expect(client.sendRequest).toHaveBeenNthCalledWith(1, 'session.tabs.createTerminal', {
worktree: 'id:wt-1',
activate: false,
select: true,
navigation: 'caller'
})
expect(client.sendRequest).toHaveBeenNthCalledWith(
2,
'session.tabs.createTerminal',
{
worktree: 'id:wt-1',
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
activate: false,
select: true,
navigation: 'caller'
},
{ timeoutMs: 30_000, budgetSpansConnect: true, expectedRuntimeId: 'r' }
)
expect(client.sendRequest).toHaveBeenNthCalledWith(3, 'terminal.send', {
expect(client.sendRequest).toHaveBeenNthCalledWith(2, 'terminal.send', {
terminal: 'term-1',
text: 'do the thing',
enter: true
@@ -52,41 +37,28 @@ describe('createTerminalAndSendPrompt', () => {
})
it('throws and skips terminal.send when createTerminal fails', async () => {
const client = clientReturning(safeHost, failure('boom'))
const client = clientReturning(failure('boom'))
await expect(createTerminalAndSendPrompt(client, 'wt-1', 'p')).rejects.toThrow('boom')
expect(client.sendRequest).toHaveBeenCalledTimes(2)
expect(client.sendRequest).toHaveBeenCalledTimes(1)
})
it('throws when the created-terminal response is malformed', async () => {
const client = clientReturning(safeHost, success({ tab: { type: 'terminal' } }))
const client = clientReturning(success({ tab: { type: 'terminal' } }))
await expect(createTerminalAndSendPrompt(client, 'wt-1', 'p')).rejects.toThrow(
'Created terminal response was invalid'
)
expect(client.sendRequest).toHaveBeenCalledTimes(2)
expect(client.sendRequest).toHaveBeenCalledTimes(1)
})
it('throws when terminal.send returns a failure', async () => {
const client = clientReturning(safeHost, createdTerminal, failure('send failed'))
const client = clientReturning(createdTerminal, failure('send failed'))
await expect(createTerminalAndSendPrompt(client, 'wt-1', 'p')).rejects.toThrow('send failed')
})
it('throws when terminal input is locked', async () => {
const client = clientReturning(
safeHost,
createdTerminal,
success({ send: { accepted: false } })
)
const client = clientReturning(createdTerminal, success({ send: { accepted: false } }))
await expect(createTerminalAndSendPrompt(client, 'wt-1', 'p')).rejects.toThrow(
'Terminal input is locked'
)
})
it('refuses an old host before terminal creation', async () => {
const client = clientReturning(success({ appVersion: '1.4.89' }))
await expect(createTerminalAndSendPrompt(client, 'wt-1', 'p')).rejects.toThrow(
'Update the host and try again'
)
expect(client.sendRequest).toHaveBeenCalledTimes(1)
})
})
+6 -21
View File
@@ -3,14 +3,6 @@ import {
readMobileReviewCreatedTerminal,
readMobileReviewTerminalSendAccepted
} from './mobile-diff-review-rpc'
import {
addLegacyTerminalAttributionDisableRequest,
withLegacyTerminalAttributionDisabledEnv
} from '../../../src/shared/legacy-terminal-attribution-env'
import {
assertMobileTerminalAttributionDisableSupported,
MOBILE_TERMINAL_CREATE_RPC_OPTIONS
} from './mobile-terminal-attribution-compat'
// Pure launch path for the PR triage actions ("Fix checks with AI" / "Resolve
// conflicts with AI"). Reuses the same two RPCs the diff-review send flow uses —
@@ -23,19 +15,12 @@ export async function createTerminalAndSendPrompt(
worktreeId: string,
prompt: string
): Promise<void> {
const authority = await assertMobileTerminalAttributionDisableSupported(client)
const created = await client.sendRequest(
'session.tabs.createTerminal',
{
worktree: `id:${worktreeId}`,
env: withLegacyTerminalAttributionDisabledEnv(undefined),
envToDelete: addLegacyTerminalAttributionDisableRequest(undefined),
activate: false,
select: true,
navigation: 'caller'
},
{ ...MOBILE_TERMINAL_CREATE_RPC_OPTIONS, expectedRuntimeId: authority.runtimeId }
)
const created = await client.sendRequest('session.tabs.createTerminal', {
worktree: `id:${worktreeId}`,
activate: false,
select: true,
navigation: 'caller'
})
if (!created.ok) {
throw new Error(created.error?.message || 'Failed to create terminal')
}
@@ -163,74 +163,6 @@ describe('useMobileDiffReviewSendActions', () => {
expect(setSendSheet).toHaveBeenCalledWith(null)
})
it('creates an unattributed terminal before sending review notes', async () => {
const sendRequest = vi
.fn()
.mockResolvedValueOnce({
id: 'status',
ok: true,
result: {
runtimeId: 'runtime',
capabilities: ['terminal.attribution-removed.v1']
},
_meta: { runtimeId: 'runtime' }
})
.mockResolvedValueOnce({
id: 'create',
ok: true,
result: { tab: { type: 'terminal', id: 'tab-1', terminal: 'terminal-1' } },
_meta: { runtimeId: 'runtime' }
})
.mockResolvedValueOnce(sendResponse(true))
await mount({ sendRequest } as unknown as RpcClient)
await act(async () => {
await actions?.createTerminalAndSend([COMMENT])
})
expect(sendRequest).toHaveBeenNthCalledWith(1, 'status.get', undefined, {
timeoutMs: 30_000,
budgetSpansConnect: true
})
expect(sendRequest).toHaveBeenNthCalledWith(
2,
'session.tabs.createTerminal',
{
worktree: 'id:wt-1',
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
activate: false,
select: true,
navigation: 'caller'
},
{ timeoutMs: 30_000, budgetSpansConnect: true, expectedRuntimeId: 'runtime' }
)
expect(sendRequest).toHaveBeenNthCalledWith(
3,
'terminal.send',
expect.objectContaining({ terminal: 'terminal-1', enter: true })
)
})
it('refuses to create review terminals on hosts that strip the bypass', async () => {
const sendRequest = vi.fn().mockResolvedValue({
id: 'status',
ok: true,
result: { appVersion: '1.4.89' },
_meta: { runtimeId: 'runtime' }
})
await mount({ sendRequest } as unknown as RpcClient)
let error: unknown
await act(async () => {
error = await actions?.createTerminalAndSend([COMMENT]).catch((err) => err)
})
expect((error as Error).message).toContain('Update the host and try again')
expect(sendRequest).toHaveBeenCalledTimes(1)
expect(sendRequest).not.toHaveBeenCalledWith('terminal.send', expect.anything())
})
it('only heals the terminal that was marked', async () => {
const sendRequest = vi.fn().mockResolvedValue(sendResponse(true))
await mount({ sendRequest } as unknown as RpcClient)
@@ -13,14 +13,6 @@ import {
} from './mobile-diff-review-rpc'
import { healMobileNativeChatStaleInput } from './mobile-native-chat-stale-input'
import type { ReviewScreenState, SendSheetState } from './mobile-diff-review-screen-model'
import {
addLegacyTerminalAttributionDisableRequest,
withLegacyTerminalAttributionDisabledEnv
} from '../../../src/shared/legacy-terminal-attribution-env'
import {
assertMobileTerminalAttributionDisableSupported,
MOBILE_TERMINAL_CREATE_RPC_OPTIONS
} from './mobile-terminal-attribution-compat'
type SendActionsInput = {
client: RpcClient | null
@@ -112,19 +104,12 @@ export function useMobileDiffReviewSendActions(input: SendActionsInput) {
if (!client || connState !== 'connected') {
throw new Error('Waiting for desktop...')
}
const authority = await assertMobileTerminalAttributionDisableSupported(client)
const response = await client.sendRequest(
'session.tabs.createTerminal',
{
worktree: `id:${worktreeId}`,
env: withLegacyTerminalAttributionDisabledEnv(undefined),
envToDelete: addLegacyTerminalAttributionDisableRequest(undefined),
activate: false,
select: true,
navigation: 'caller'
},
{ ...MOBILE_TERMINAL_CREATE_RPC_OPTIONS, expectedRuntimeId: authority.runtimeId }
)
const response = await client.sendRequest('session.tabs.createTerminal', {
worktree: `id:${worktreeId}`,
activate: false,
select: true,
navigation: 'caller'
})
if (!response.ok) {
throw new Error(response.error?.message || 'Failed to create terminal')
}
+1 -10
View File
@@ -54,7 +54,6 @@ type ConnectWaiter = {
export type SendRequestOptions = {
timeoutMs?: number
expectedRuntimeId?: string
/** Spend `timeoutMs` across connect-wait AND the request instead of giving each
* phase its own. Interactive chat writes need it: they run as sequential loops
* under one shared budget, so a per-phase clock lets the composer sit `sending`
@@ -1014,15 +1013,7 @@ export function connect(
}
})
if (
!sendEncrypted({
id,
deviceToken,
method,
params,
...(options?.expectedRuntimeId ? { expectedRuntimeId: options.expectedRuntimeId } : {})
})
) {
if (!sendEncrypted({ id, deviceToken, method, params })) {
pending.delete(id)
clearTimeout(timeout)
reject(new Error('Connection interrupted'))
-1
View File
@@ -18,7 +18,6 @@ export type RpcRequest = {
deviceToken: string
method: string
params?: unknown
expectedRuntimeId?: string
}
export type RpcSuccess = {
-64
View File
@@ -61,67 +61,3 @@ describe('terminal close CLI', () => {
expect(help).toContain('durable persistence')
})
})
describe('terminal split CLI', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('sends the old-host attribution bypass through the legacy-compatible env field', async () => {
const call = vi.fn(async (method: string) =>
method === 'status.get'
? {
result: {
runtimeId: 'runtime-1',
capabilities: ['terminal.attribution-removed.v1']
}
}
: { result: { split: { handle: 'term-2', parentHandle: 'term-1' } } }
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await TERMINAL_HANDLERS['terminal split']({
flags: new Map([
['terminal', 'term-1'],
['direction', 'horizontal']
]),
client: { call } as unknown as RuntimeClient,
cwd: '/tmp/worktree',
json: true
})
expect(call).toHaveBeenNthCalledWith(1, 'status.get')
expect(call).toHaveBeenNthCalledWith(
2,
'terminal.split',
{
terminal: 'term-1',
direction: 'horizontal',
command: undefined,
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION']
},
{ expectedRuntimeId: 'runtime-1' }
)
})
it('refuses legacy hosts because renderer-owned splits discard environment fields', async () => {
const call = vi.fn().mockResolvedValue({
result: { appVersion: '1.4.181', capabilities: ['mobile.tasks.v1'] }
})
await expect(
TERMINAL_HANDLERS['terminal split']({
flags: new Map([['terminal', 'term-1']]),
client: { call } as unknown as RuntimeClient,
cwd: '/tmp/worktree',
json: true
})
).rejects.toMatchObject({
code: 'runtime_update_required',
message: expect.stringContaining('Update the host and try again')
})
expect(call).toHaveBeenCalledTimes(1)
expect(call).not.toHaveBeenCalledWith('terminal.split', expect.anything())
})
})
+17 -52
View File
@@ -8,8 +8,7 @@ import type {
RuntimeTerminalSend,
RuntimeTerminalShow,
RuntimeTerminalSplit,
RuntimeTerminalWait,
RuntimeStatus
RuntimeTerminalWait
} from '../../shared/runtime-types'
import type { CommandHandler } from '../dispatch'
import { shouldUseRendererBackedInteractiveTerminal } from '../codex-command-classification'
@@ -38,14 +37,6 @@ import {
getRequiredWorktreeSelector,
getTerminalHandle
} from '../selectors'
import {
addLegacyTerminalAttributionDisableRequest,
hostSupportsTerminalCreateAttributionDisable,
hostSupportsTerminalSplitAttributionDisable,
TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE,
TERMINAL_SPLIT_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE,
withLegacyTerminalAttributionDisabledEnv
} from '../../shared/legacy-terminal-attribution-env'
// Why: terminal wait legitimately needs to outlive the CLI's default RPC
// timeout. Even without an explicit server timeout, the client must allow
@@ -146,30 +137,17 @@ export const TERMINAL_HANDLERS: Record<string, CommandHandler> = {
const useRendererBackedInteractiveTerminal =
!client.isRemote && shouldUseRendererBackedInteractiveTerminal(command)
const focus = flags.get('focus') === true
const status = await client.call<RuntimeStatus>('status.get')
if (!hostSupportsTerminalCreateAttributionDisable(status.result)) {
throw new RuntimeClientError(
'runtime_update_required',
TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE
)
}
const result = await client.call<{ terminal: RuntimeTerminalCreate }>(
'terminal.create',
{
worktree: await getBrowserWorktreeSelector(flags, cwd, client),
command,
env: withLegacyTerminalAttributionDisabledEnv(undefined),
envToDelete: addLegacyTerminalAttributionDisableRequest(undefined),
title: getOptionalStringFlag(flags, 'title'),
// Why: interactive local agent TUIs need the renderer-backed terminal
// path for browser-side features, but CLI creates must stay backgrounded
// unless the caller explicitly asks for focus.
focus,
...(focus ? { presentation: 'focused' } : {}),
...(useRendererBackedInteractiveTerminal ? { rendererBacked: true, activate: focus } : {})
},
{ expectedRuntimeId: status.result.runtimeId }
)
const result = await client.call<{ terminal: RuntimeTerminalCreate }>('terminal.create', {
worktree: await getBrowserWorktreeSelector(flags, cwd, client),
command,
title: getOptionalStringFlag(flags, 'title'),
// Why: interactive local agent TUIs need the renderer-backed terminal
// path for browser-side features, but CLI creates must stay backgrounded
// unless the caller explicitly asks for focus.
focus,
...(focus ? { presentation: 'focused' } : {}),
...(useRendererBackedInteractiveTerminal ? { rendererBacked: true, activate: focus } : {})
})
printResult(result, json, formatTerminalCreate)
},
// `focus` resolves to this canonical path via CommandSpec.aliases before dispatch.
@@ -190,24 +168,11 @@ export const TERMINAL_HANDLERS: Record<string, CommandHandler> = {
) {
throw new RuntimeClientError('invalid_argument', '--direction must be horizontal or vertical')
}
const status = await client.call<RuntimeStatus>('status.get')
if (!hostSupportsTerminalSplitAttributionDisable(status.result)) {
throw new RuntimeClientError(
'runtime_update_required',
TERMINAL_SPLIT_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE
)
}
const result = await client.call<{ split: RuntimeTerminalSplit }>(
'terminal.split',
{
terminal: await getTerminalHandle(flags, cwd, client),
direction: directionFlag,
command: getOptionalStringFlag(flags, 'command'),
env: withLegacyTerminalAttributionDisabledEnv(undefined),
envToDelete: addLegacyTerminalAttributionDisableRequest(undefined)
},
{ expectedRuntimeId: status.result.runtimeId }
)
const result = await client.call<{ split: RuntimeTerminalSplit }>('terminal.split', {
terminal: await getTerminalHandle(flags, cwd, client),
direction: directionFlag,
command: getOptionalStringFlag(flags, 'command')
})
printResult(result, json, formatTerminalSplit)
}
}
+82 -202
View File
@@ -3112,10 +3112,6 @@ describe('orca cli worktree awareness', () => {
it('passes explicit focus through terminal.create', async () => {
queueFixtures(
callMock,
okFixture('req_status', {
runtimeId: 'runtime-1',
capabilities: ['terminal.attribution-removed.v1']
}),
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
@@ -3140,19 +3136,13 @@ describe('orca cli worktree awareness', () => {
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith(
'terminal.create',
{
worktree: 'path:/tmp/repo/feature',
command: undefined,
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
title: 'RUNNER',
focus: true,
presentation: 'focused'
},
{ expectedRuntimeId: 'runtime-1' }
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: undefined,
title: 'RUNNER',
focus: true,
presentation: 'focused'
})
})
it('prints terminal.read fallback screen lines in json mode', async () => {
@@ -3194,10 +3184,6 @@ describe('orca cli worktree awareness', () => {
it('keeps interactive Codex startup commands backgrounded unless focus is explicit', async () => {
queueFixtures(
callMock,
okFixture('req_status', {
runtimeId: 'runtime-1',
capabilities: ['terminal.attribution-removed.v1']
}),
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
@@ -3223,29 +3209,19 @@ describe('orca cli worktree awareness', () => {
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith(
'terminal.create',
{
worktree: 'path:/tmp/repo/feature',
command: 'codex',
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
title: 'Codex',
focus: false,
rendererBacked: true,
activate: false
},
{ expectedRuntimeId: 'runtime-1' }
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'codex',
title: 'Codex',
focus: false,
rendererBacked: true,
activate: false
})
})
it('keeps explicit focus semantics when forcing Codex through the renderer path', async () => {
queueFixtures(
callMock,
okFixture('req_status', {
runtimeId: 'runtime-1',
capabilities: ['terminal.attribution-removed.v1']
}),
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
@@ -3272,30 +3248,20 @@ describe('orca cli worktree awareness', () => {
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith(
'terminal.create',
{
worktree: 'path:/tmp/repo/feature',
command: 'codex',
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
title: 'Codex',
focus: true,
presentation: 'focused',
rendererBacked: true,
activate: true
},
{ expectedRuntimeId: 'runtime-1' }
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'codex',
title: 'Codex',
focus: true,
presentation: 'focused',
rendererBacked: true,
activate: true
})
})
it('does not force the visible terminal path for explicit Codex exec commands', async () => {
queueFixtures(
callMock,
okFixture('req_status', {
runtimeId: 'runtime-1',
capabilities: ['terminal.attribution-removed.v1']
}),
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
@@ -3321,27 +3287,17 @@ describe('orca cli worktree awareness', () => {
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith(
'terminal.create',
{
worktree: 'path:/tmp/repo/feature',
command: 'codex exec summarize',
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
title: 'Codex exec',
focus: false
},
{ expectedRuntimeId: 'runtime-1' }
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'codex exec summarize',
title: 'Codex exec',
focus: false
})
})
it('does not force the visible terminal path for Codex exec commands after global options', async () => {
queueFixtures(
callMock,
okFixture('req_status', {
runtimeId: 'runtime-1',
capabilities: ['terminal.attribution-removed.v1']
}),
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
@@ -3367,27 +3323,17 @@ describe('orca cli worktree awareness', () => {
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith(
'terminal.create',
{
worktree: 'path:/tmp/repo/feature',
command: 'codex -m gpt-5 --sandbox workspace-write exec summarize',
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
title: 'Codex exec',
focus: false
},
{ expectedRuntimeId: 'runtime-1' }
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'codex -m gpt-5 --sandbox workspace-write exec summarize',
title: 'Codex exec',
focus: false
})
})
it('does not force the visible terminal path for Codex review commands after long options', async () => {
queueFixtures(
callMock,
okFixture('req_status', {
runtimeId: 'runtime-1',
capabilities: ['terminal.attribution-removed.v1']
}),
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
@@ -3413,27 +3359,17 @@ describe('orca cli worktree awareness', () => {
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith(
'terminal.create',
{
worktree: 'path:/tmp/repo/feature',
command: 'codex --model=gpt-5 --sandbox=workspace-write review',
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
title: 'Codex review',
focus: false
},
{ expectedRuntimeId: 'runtime-1' }
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'codex --model=gpt-5 --sandbox=workspace-write review',
title: 'Codex review',
focus: false
})
})
it('does not force the visible terminal path for Codex help commands', async () => {
queueFixtures(
callMock,
okFixture('req_status', {
runtimeId: 'runtime-1',
capabilities: ['terminal.attribution-removed.v1']
}),
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
@@ -3459,27 +3395,17 @@ describe('orca cli worktree awareness', () => {
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith(
'terminal.create',
{
worktree: 'path:/tmp/repo/feature',
command: 'codex --help',
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
title: 'Codex help',
focus: false
},
{ expectedRuntimeId: 'runtime-1' }
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'codex --help',
title: 'Codex help',
focus: false
})
})
it('keeps Codex prompts after global options backgrounded unless focus is explicit', async () => {
queueFixtures(
callMock,
okFixture('req_status', {
runtimeId: 'runtime-1',
capabilities: ['terminal.attribution-removed.v1']
}),
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
@@ -3505,29 +3431,19 @@ describe('orca cli worktree awareness', () => {
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith(
'terminal.create',
{
worktree: 'path:/tmp/repo/feature',
command: 'codex -m gpt-5 "fix the flaky test"',
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
title: 'Codex prompt',
focus: false,
rendererBacked: true,
activate: false
},
{ expectedRuntimeId: 'runtime-1' }
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'codex -m gpt-5 "fix the flaky test"',
title: 'Codex prompt',
focus: false,
rendererBacked: true,
activate: false
})
})
it('keeps interactive Claude startup commands backgrounded unless focus is explicit', async () => {
queueFixtures(
callMock,
okFixture('req_status', {
runtimeId: 'runtime-1',
capabilities: ['terminal.attribution-removed.v1']
}),
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
@@ -3553,29 +3469,19 @@ describe('orca cli worktree awareness', () => {
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith(
'terminal.create',
{
worktree: 'path:/tmp/repo/feature',
command: 'claude',
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
title: 'Claude',
focus: false,
rendererBacked: true,
activate: false
},
{ expectedRuntimeId: 'runtime-1' }
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'claude',
title: 'Claude',
focus: false,
rendererBacked: true,
activate: false
})
})
it('keeps Claude print commands on the background terminal path', async () => {
queueFixtures(
callMock,
okFixture('req_status', {
runtimeId: 'runtime-1',
capabilities: ['terminal.attribution-removed.v1']
}),
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
@@ -3601,18 +3507,12 @@ describe('orca cli worktree awareness', () => {
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith(
'terminal.create',
{
worktree: 'path:/tmp/repo/feature',
command: 'claude -p "summarize"',
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
title: 'Claude print',
focus: false
},
{ expectedRuntimeId: 'runtime-1' }
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'claude -p "summarize"',
title: 'Claude print',
focus: false
})
})
it('uses the resolved enclosing worktree for other worktree consumers', async () => {
@@ -3919,10 +3819,6 @@ describe('orca cli worktree awareness', () => {
it('sends explicit remote terminal create worktree selectors unchanged', async () => {
queueFixtures(
callMock,
okFixture('req_status', {
runtimeId: 'runtime-1',
capabilities: ['terminal.attribution-removed.v1']
}),
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
@@ -3946,18 +3842,12 @@ describe('orca cli worktree awareness', () => {
'/tmp/client/repo/src'
)
expect(callMock).toHaveBeenCalledWith(
'terminal.create',
{
worktree: 'id:repo-1::/srv/orca/feature',
command: undefined,
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
title: undefined,
focus: false
},
{ expectedRuntimeId: 'runtime-1' }
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'id:repo-1::/srv/orca/feature',
command: undefined,
title: undefined,
focus: false
})
})
it('collects and formats memory diagnostics', async () => {
@@ -4065,10 +3955,6 @@ describe('orca cli worktree awareness', () => {
it('does not force remote Codex terminal creates through a local renderer path', async () => {
queueFixtures(
callMock,
okFixture('req_status', {
runtimeId: 'runtime-1',
capabilities: ['terminal.attribution-removed.v1']
}),
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
@@ -4096,18 +3982,12 @@ describe('orca cli worktree awareness', () => {
'/tmp/client/repo/src'
)
expect(callMock).toHaveBeenCalledWith(
'terminal.create',
{
worktree: 'id:repo-1::/srv/orca/feature',
command: 'codex',
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
title: 'Codex',
focus: false
},
{ expectedRuntimeId: 'runtime-1' }
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'id:repo-1::/srv/orca/feature',
command: 'codex',
title: 'Codex',
focus: false
})
})
it('does not resolve implicit remote browser targets from client cwd', async () => {
-1
View File
@@ -87,7 +87,6 @@ export class RuntimeClient {
}
: {}
const envelope = {
expectedRuntimeId: options?.expectedRuntimeId,
orchestrationCapability: options?.orchestrationCapability,
orchestrationContractVersion: method.startsWith('orchestration.')
? ORCHESTRATION_CONTRACT_VERSION
-42
View File
@@ -50,48 +50,6 @@ describe('runtime transport timeout validation', () => {
// Why: these tests create Unix domain socket servers in temp directories.
// Windows does not support Unix domain sockets in the same way.
describe.skipIf(process.platform === 'win32')('runtime transport', () => {
it('forwards the expected runtime fence in local requests', async () => {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-transport-'))
const endpoint = join(userDataPath, 'runtime.sock')
let receivedRequest: Record<string, unknown> | null = null
const server = createServer((socket) => {
sockets.add(socket)
socket.once('close', () => sockets.delete(socket))
socket.once('data', (data) => {
receivedRequest = JSON.parse(String(data).trim()) as Record<string, unknown>
socket.write(
`${JSON.stringify({
id: receivedRequest.id,
ok: true,
result: { terminal: { handle: 'terminal-1' } },
_meta: { runtimeId: 'runtime-1' }
})}\n`
)
})
})
servers.add(server)
await new Promise<void>((resolve) => server.listen(endpoint, resolve))
await sendRequest(
{
runtimeId: 'runtime-1',
pid: 123,
transports: [{ kind: 'unix', endpoint }],
authToken: 'token',
startedAt: 1
},
'terminal.create',
{ worktree: 'id:worktree-1' },
1_000,
{ expectedRuntimeId: 'runtime-1' }
)
expect(receivedRequest).toMatchObject({
method: 'terminal.create',
expectedRuntimeId: 'runtime-1'
})
})
it('refreshes the per-call timeout when the runtime sends keepalive frames', async () => {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-transport-'))
const endpoint = join(userDataPath, 'runtime.sock')
-1
View File
@@ -186,7 +186,6 @@ export async function sendRequest<TResult>(
authToken: metadata.authToken,
method,
params,
expectedRuntimeId: envelope?.expectedRuntimeId,
orchestrationCapability: envelope?.orchestrationCapability,
orchestrationContractVersion: envelope?.orchestrationContractVersion,
orchestrationRequestId: envelope?.orchestrationRequestId,
@@ -183,7 +183,6 @@ function registerPassiveCallHandler(getUserDataPath: () => string): void {
params?: unknown
timeoutMs?: number
expectedEnvironmentPairingRevision?: number
expectedRuntimeId?: string
}
): Promise<RuntimeRpcResponse<unknown>> => {
const environment = resolveEnvironment(getUserDataPath(), args.selector)
@@ -198,8 +197,7 @@ function registerPassiveCallHandler(getUserDataPath: () => string): void {
args.method,
args.params,
args.timeoutMs,
args.expectedEnvironmentPairingRevision,
args.expectedRuntimeId ? { expectedRuntimeId: args.expectedRuntimeId } : undefined
args.expectedEnvironmentPairingRevision
)
} catch (error) {
const failure = runtimeEnvironmentCallFailure(environment, args.method, error)
@@ -77,15 +77,13 @@ export function subscribeRemoteRuntimeSharedControlRequest<TResult>(
onBinary?: (bytes: Uint8Array<ArrayBufferLike>) => void
onError: (error: { code: string; message: string }) => void
onClose?: () => void
},
envelope?: RuntimeOrchestrationEnvelope
}
): Promise<RemoteRuntimeSharedSubscription> {
return getSharedControlConnection(environmentId, pairing).subscribe(
method,
params,
timeoutMs,
callbacks,
envelope?.expectedRuntimeId
callbacks
)
}
@@ -193,14 +193,11 @@ export async function subscribeRuntimeEnvironment(
| { type: 'close' }
) => void
onClose: () => void
},
expectedRuntimeId?: string
}
): Promise<RemoteRuntimeSubscription> {
const environment = resolveEnvironment(userDataPath, selector)
const pairing = getPreferredPairingOffer(environment)
const effectiveTimeoutMs = timeoutMs ?? DEFAULT_REMOTE_RUNTIME_TIMEOUT_MS
const envelope: RuntimeOrchestrationEnvelope | undefined =
expectedRuntimeId === undefined ? undefined : { expectedRuntimeId }
let markedUsed = false
const markUsedOnce = (runtimeId: string): void => {
if (markedUsed) {
@@ -243,8 +240,7 @@ export async function subscribeRuntimeEnvironment(
method,
params,
effectiveTimeoutMs,
callbacksWithMarkUsed,
envelope
callbacksWithMarkUsed
)
}
return await subscribeRemoteRuntimeRequest(
@@ -252,9 +248,7 @@ export async function subscribeRuntimeEnvironment(
method,
params,
effectiveTimeoutMs,
callbacksWithMarkUsed,
undefined,
envelope
callbacksWithMarkUsed
)
} catch (error) {
if (error instanceof Error) {
+9 -39
View File
@@ -1094,13 +1094,7 @@ describe('registerRuntimeEnvironmentHandlers', () => {
await add(null, { name: 'desk', pairingCode: pairingCode() })
const subscribe = handler<
{
selector: string
method: string
params?: unknown
subscriptionId?: string
expectedRuntimeId?: string
},
{ selector: string; method: string; params?: unknown; subscriptionId?: string },
{ subscriptionId: string; requestId: string }
>('runtimeEnvironments:subscribe')
await subscribe(
@@ -1113,12 +1107,7 @@ describe('registerRuntimeEnvironmentHandlers', () => {
removeListener: vi.fn()
}
},
{
selector: 'desk',
method: 'browser.screencast',
params: { pageId: 'page-1' },
expectedRuntimeId: 'runtime-1'
}
{ selector: 'desk', method: 'browser.screencast', params: { pageId: 'page-1' } }
)
await subscribe(
{
@@ -1138,18 +1127,14 @@ describe('registerRuntimeEnvironmentHandlers', () => {
'browser.screencast',
{ pageId: 'page-1' },
15_000,
expect.any(Object),
undefined,
{ expectedRuntimeId: 'runtime-1' }
expect.any(Object)
)
expect(subscribeRemoteRuntimeRequestMock).toHaveBeenCalledWith(
expect.any(Object),
'terminal.multiplex',
{ client: { id: 'client-1' } },
15_000,
expect.any(Object),
undefined,
undefined
expect.any(Object)
)
expect(subscribeRemoteRuntimeSharedControlRequestMock).not.toHaveBeenCalled()
})
@@ -1178,13 +1163,7 @@ describe('registerRuntimeEnvironmentHandlers', () => {
await add(null, { name: 'desk', pairingCode: pairingCode() })
const subscribe = handler<
{
selector: string
method: string
params?: unknown
subscriptionId?: string
expectedRuntimeId?: string
},
{ selector: string; method: string; params?: unknown; subscriptionId?: string },
{ subscriptionId: string; requestId: string }
>('runtimeEnvironments:subscribe')
await expect(
@@ -1198,11 +1177,7 @@ describe('registerRuntimeEnvironmentHandlers', () => {
removeListener: vi.fn()
}
},
{
selector: 'desk',
method: 'session.tabs.subscribeAll',
expectedRuntimeId: 'runtime-1'
}
{ selector: 'desk', method: 'session.tabs.subscribeAll' }
)
).resolves.toMatchObject({ requestId: 'tabs-shared' })
@@ -1212,8 +1187,7 @@ describe('registerRuntimeEnvironmentHandlers', () => {
'session.tabs.subscribeAll',
undefined,
15_000,
expect.any(Object),
{ expectedRuntimeId: 'runtime-1' }
expect.any(Object)
)
expect(subscribeRemoteRuntimeRequestMock).not.toHaveBeenCalled()
})
@@ -1341,9 +1315,7 @@ describe('registerRuntimeEnvironmentHandlers', () => {
'session.tabs.subscribeAll',
undefined,
15_000,
expect.any(Object),
undefined,
undefined
expect.any(Object)
)
expect(subscribeRemoteRuntimeSharedControlRequestMock).not.toHaveBeenCalled()
})
@@ -1687,9 +1659,7 @@ describe('registerRuntimeEnvironmentHandlers', () => {
'terminal.subscribe',
{ terminal: 't1' },
25,
expect.any(Object),
undefined,
undefined
expect.any(Object)
)
expect(sent).toEqual([
expect.objectContaining({ subscriptionId: result.subscriptionId, type: 'response' }),
+1 -3
View File
@@ -90,7 +90,6 @@ export function registerRuntimeEnvironmentHandlers(store: Store): void {
timeoutMs?: number
subscriptionId?: string
expectedEnvironmentPairingRevision?: number
expectedRuntimeId?: string
}
): Promise<{ subscriptionId: string; requestId: string }> => {
const subscriptionId =
@@ -181,8 +180,7 @@ export function registerRuntimeEnvironmentHandlers(store: Store): void {
retained?.removeDestroyedListener()
remoteRuntimeSubscriptions.delete(subscriptionId)
}
},
args.expectedRuntimeId
}
)
} catch (error) {
removeDestroyedListener()
+57 -3
View File
@@ -66,12 +66,17 @@ describe('legacy terminal shim neutralization', () => {
expect(statSync(path).mode & 0o111).not.toBe(0)
}
}
expect(readFileSync(join(legacyRoot, 'VERSION'), 'utf8')).toBe('7\n')
// Why: must not equal the retired shim's own '7', or a rolled-back build treats its wrappers
// as current and never rewrites them.
const version = readFileSync(join(legacyRoot, 'VERSION'), 'utf8')
expect(version).toBe('7-neutralized\n')
expect(version.trim()).not.toBe('7')
})
it('rejects stale Windows real-command paths inside the wrapper directory', () => {
const userData = makeUserDataDir()
const win32Dir = join(userData, 'orca-terminal-attribution', 'win32')
mkdirSync(win32Dir, { recursive: true })
neutralizeLegacyTerminalShimDir(userData)
@@ -79,6 +84,10 @@ describe('legacy terminal shim neutralization', () => {
expect(cmd).toContain(
'if defined orca_real for %%G in ("%orca_real%") do if /I "%%~dpG"=="%~dp0" set "orca_real="'
)
// Why: a captured path that no longer exists must be cleared, or the where.exe fallback below
// is skipped and the wrapper execs a missing binary.
expect(cmd).toContain('if defined orca_real if not exist "%orca_real%" set "orca_real="')
expect(cmd.indexOf('if not exist "%orca_real%"')).toBeLessThan(cmd.indexOf('where.exe git.exe'))
const powershell = readFileSync(join(win32Dir, 'git-wrapper.ps1'), 'utf8')
expect(powershell).toContain('[StringComparison]::OrdinalIgnoreCase')
expect(powershell).toContain('$realCommand = $null')
@@ -90,6 +99,7 @@ describe('legacy terminal shim neutralization', () => {
it('removes every Windows PATH occurrence of both captured wrapper directories', () => {
const userData = makeUserDataDir()
const win32Dir = join(userData, 'orca-terminal-attribution', 'win32')
mkdirSync(win32Dir, { recursive: true })
neutralizeLegacyTerminalShimDir(userData)
@@ -111,11 +121,13 @@ describe('legacy terminal shim neutralization', () => {
expect(powershell).toContain('[StringComparison]::OrdinalIgnoreCase')
})
it('does not throw when the legacy directory is absent', () => {
it('leaves an install that never ran the shim untouched', () => {
// Why: a clean install has no resolved wrapper paths to keep alive, so writing tombstones
// there would recreate the very directory the removal deleted.
const userData = makeUserDataDir()
expect(() => neutralizeLegacyTerminalShimDir(userData)).not.toThrow()
expect(existsSync(join(userData, 'orca-terminal-attribution', 'posix', 'git'))).toBe(true)
expect(existsSync(join(userData, 'orca-terminal-attribution'))).toBe(false)
})
itOnPosixNonRoot('retries a startup failure in-process and latches after success', async () => {
@@ -145,6 +157,48 @@ describe('legacy terminal shim neutralization', () => {
}
})
itOnPosixNonRoot('warns and stops retrying once the ladder is exhausted', async () => {
vi.useFakeTimers()
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const userData = makeUserDataDir()
const posixDir = join(userData, 'orca-terminal-attribution', 'posix')
mkdirSync(posixDir, { recursive: true })
writeFileSync(join(posixDir, 'git'), 'legacy attribution wrapper')
// Why: keep every attempt failing so the ladder runs to exhaustion.
chmodSync(posixDir, 0o500)
try {
neutralizeLegacyTerminalShimDir(userData)
// 1s + 5s + 15s + 30s covers every configured delay, plus slack for a fifth that must not fire.
await vi.advanceTimersByTimeAsync(120_000)
expect(readFileSync(join(posixDir, 'git'), 'utf8')).toBe('legacy attribution wrapper')
const messages = warn.mock.calls.map((call) => String(call[0]))
expect(messages.filter((message) => message.includes('neutralization attempt'))).toHaveLength(
5
)
// Why: pin the ordinals too — the count alone would not catch an off-by-one.
expect(messages.some((message) => message.includes('neutralization attempt 1 failed'))).toBe(
true
)
expect(messages.some((message) => message.includes('neutralization attempt 5 failed'))).toBe(
true
)
// Why: the give-up count must agree with the last per-attempt line, not the retry counter.
expect(
messages.some((message) => message.includes('gave up neutralizing after 5 attempts'))
).toBe(true)
// Exhausted means quiet: no further timers, so no further warnings.
warn.mockClear()
await vi.advanceTimersByTimeAsync(120_000)
expect(warn).not.toHaveBeenCalled()
} finally {
chmodSync(posixDir, 0o700)
warn.mockRestore()
vi.useRealTimers()
}
})
itOnPosix('keeps a real Bash command hash working with trailing PATH separators', async () => {
const userData = makeUserDataDir()
const shimDir = join(userData, 'orca-terminal-attribution', 'posix')
+39 -14
View File
@@ -1,13 +1,15 @@
import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'
import { chmodSync, existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import {
LEGACY_TERMINAL_ATTRIBUTION_BYPASS_ENV_KEY,
LEGACY_TERMINAL_ATTRIBUTION_ENABLE_ENV_KEY
} from '../../shared/legacy-terminal-attribution-env'
import { renderLegacyTerminalPosixTombstone } from './legacy-terminal-posix-tombstone'
const LEGACY_TERMINAL_ATTRIBUTION_ENABLE_ENV_KEY = 'ORCA_ENABLE_GIT_ATTRIBUTION'
const LEGACY_TERMINAL_ATTRIBUTION_BYPASS_ENV_KEY = 'ORCA_ATTRIBUTION_BYPASS'
const LEGACY_SHIM_ROOT_DIR = 'orca-terminal-attribution'
const LEGACY_SHIM_VERSION = '7'
// Why: must differ from the retired shim's own '7'. A rolled-back build compares this marker and
// skips rewriting its wrappers when it matches, which would leave our tombstones in place while
// its attribution toggle claimed to be on.
const LEGACY_SHIM_VERSION = '7-neutralized'
const NEUTRALIZATION_RETRY_DELAYS_MS = [1_000, 5_000, 15_000, 30_000]
export const LEGACY_TERMINAL_SHIM_ENV_KEYS = [
'ORCA_ENABLE_GIT_ATTRIBUTION',
@@ -40,7 +42,9 @@ set "ORCA_ATTRIBUTION_BYPASS="
set "ORCA_REAL_GIT="
set "ORCA_REAL_GH="
if defined orca_real for %%G in ("%orca_real%") do if /I "%%~dpG"=="%~dp0" set "orca_real="
if defined orca_real if exist "%orca_real%" goto run
rem Why: clear a captured path that no longer exists, or the where.exe fallback below is skipped.
if defined orca_real if not exist "%orca_real%" set "orca_real="
if defined orca_real goto run
for /f "delims=" %%G in ('where.exe __ORCA_COMMAND__.exe 2^>nul') do if not defined orca_real set "orca_real=%%G"
if not defined orca_real (
echo Orca compatibility wrapper could not locate __ORCA_COMMAND__ on PATH. 1>&2
@@ -101,23 +105,40 @@ export function neutralizeLegacyTerminalShimDir(userDataPath: string): void {
if (neutralized) {
return
}
const rootDir = join(userDataPath, LEGACY_SHIM_ROOT_DIR)
// Why: only installs that actually ran the shim have resolved wrapper paths worth keeping
// alive. Writing them anywhere else would recreate the directory the removal deleted.
if (!existsSync(rootDir)) {
neutralized = true
clearNeutralizationRetry()
return
}
try {
const rootDir = join(userDataPath, LEGACY_SHIM_ROOT_DIR)
writeNeutralWrappers(rootDir)
writeFileAtomically(join(rootDir, 'VERSION'), `${LEGACY_SHIM_VERSION}\n`, 0o644)
neutralized = true
clearNeutralizationRetry()
} catch {
} catch (error) {
// Why: Windows can keep a running .cmd open briefly after startup.
console.warn(
`[legacy-terminal-shim] neutralization attempt ${neutralizationRetryAttempt + 1} failed:`,
error instanceof Error ? error.message : String(error)
)
scheduleNeutralizationRetry(userDataPath)
}
}
function scheduleNeutralizationRetry(userDataPath: string): void {
if (
neutralizationRetryTimer ||
neutralizationRetryAttempt >= NEUTRALIZATION_RETRY_DELAYS_MS.length
) {
if (neutralizationRetryTimer) {
return
}
if (neutralizationRetryAttempt >= NEUTRALIZATION_RETRY_DELAYS_MS.length) {
// Why: retries stop here for the process lifetime; without this the give-up is invisible and a
// host left holding live wrappers is undiagnosable.
// Why: +1 counts the initial attempt, so this agrees with the per-attempt line above.
console.warn(
`[legacy-terminal-shim] gave up neutralizing after ${neutralizationRetryAttempt + 1} attempts; legacy git/gh wrappers may remain until the next launch`
)
return
}
const delayMs = NEUTRALIZATION_RETRY_DELAYS_MS[neutralizationRetryAttempt]
@@ -172,7 +193,11 @@ function writeFileAtomically(filePath: string, contents: string, mode: number):
chmodSync(temporaryPath, mode)
renameSync(temporaryPath, filePath)
} finally {
rmSync(temporaryPath, { force: true, recursive: true })
try {
rmSync(temporaryPath, { force: true, recursive: true })
} catch {
// Why: a locked temp file must not replace the in-flight error with a cleanup error.
}
}
}
-1
View File
@@ -46,7 +46,6 @@ export type RpcRequest = {
authToken: string
method: string
params?: unknown
expectedRuntimeId?: string
orchestrationCapability?: string
orchestrationContractVersion?: number
orchestrationRequestId?: string
@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from 'vitest'
import { describe, expect, it } from 'vitest'
import { z } from 'zod'
import { RpcDispatcher } from './dispatcher'
import { defineMethod, InvalidArgumentError, type RpcRequest } from './core'
@@ -41,26 +41,6 @@ const METHODS = [
]
describe('RpcDispatcher computer-use validation errors', () => {
it('rejects a replacement runtime before invoking a one-shot handler', async () => {
const handler = vi.fn(() => ({ ok: true }))
const dispatcher = new RpcDispatcher({
runtime: makeRuntime(),
methods: [defineMethod({ name: 'terminal.create', params: z.object({}), handler })]
})
const response = await dispatcher.dispatch({
...makeRequest('terminal.create', {}),
expectedRuntimeId: 'previous-runtime'
})
expect(response).toMatchObject({
ok: false,
error: { code: 'runtime_replaced' },
_meta: { runtimeId: 'test-runtime' }
})
expect(handler).not.toHaveBeenCalled()
})
it('adds recovery steps to one-shot computer schema failures', async () => {
const dispatcher = new RpcDispatcher({ runtime: makeRuntime(), methods: METHODS })
-21
View File
@@ -43,14 +43,6 @@ export class RpcDispatcher {
async dispatch(request: RpcRequest, options?: { signal?: AbortSignal }): Promise<RpcResponse> {
const meta = this.meta()
if (request.expectedRuntimeId && request.expectedRuntimeId !== meta.runtimeId) {
return errorResponse(
request.id,
meta,
'runtime_replaced',
'The runtime changed before the request could be applied. Refresh and try again.'
)
}
const method = this.registry.get(request.method)
if (!method) {
return errorResponse(
@@ -147,19 +139,6 @@ export class RpcDispatcher {
options?: RpcDispatchStreamingOptions
): Promise<void> {
const meta = this.meta()
if (request.expectedRuntimeId && request.expectedRuntimeId !== meta.runtimeId) {
reply(
JSON.stringify(
errorResponse(
request.id,
meta,
'runtime_replaced',
'The runtime changed before the request could be applied. Refresh and try again.'
)
)
)
return
}
const method = this.registry.get(request.method)
if (!method) {
reply(
@@ -1,37 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type { OrcaRuntimeService } from '../../orca-runtime'
import type { RpcRequest } from '../core'
import { RpcDispatcher } from '../dispatcher'
import { TERMINAL_METHODS } from './terminal'
describe('terminal split environment forwarding', () => {
it('forwards requested environment deletions to the runtime owner', async () => {
const splitTerminal = vi.fn().mockResolvedValue({ handle: 'term-split' })
const runtime = {
getRuntimeId: () => 'runtime-1',
splitTerminal
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: TERMINAL_METHODS })
const request: RpcRequest = {
id: 'split-1',
authToken: 'token',
method: 'terminal.split',
params: {
terminal: 'term-1',
direction: 'horizontal',
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION']
}
}
await dispatcher.dispatch(request)
expect(splitTerminal).toHaveBeenCalledWith('term-1', {
direction: 'horizontal',
command: undefined,
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
telemetrySource: undefined
})
})
})
-2
View File
@@ -985,7 +985,6 @@ const TerminalSplit = TerminalHandle.extend({
.optional(),
command: OptionalString,
env: z.record(z.string(), z.string()).optional(),
envToDelete: z.array(z.string().min(1).max(256)).max(32).optional(),
telemetrySource: z.enum(TERMINAL_PANE_SPLIT_SOURCES).optional()
})
@@ -1473,7 +1472,6 @@ export const TERMINAL_METHODS: RpcAnyMethod[] = [
direction: params.direction,
command: params.command,
env: params.env,
envToDelete: params.envToDelete,
telemetrySource: params.telemetrySource
})
})
-22
View File
@@ -231,28 +231,6 @@ describe('RpcDispatcher streaming', () => {
expect(response.streaming).toBeUndefined()
})
it('rejects a replacement runtime before streaming dispatch invokes a mutation', async () => {
const messages: string[] = []
const handler = vi.fn(() => ({ terminal: { handle: 'terminal-1' } }))
const dispatcher = new RpcDispatcher({
runtime: stubRuntime(),
methods: [defineMethod({ name: 'terminal.create', params: z.object({}), handler })]
})
await dispatcher.dispatchStreaming(
{ ...makeRequest('terminal.create', {}), expectedRuntimeId: 'previous-runtime' },
(message) => messages.push(message)
)
expect(messages).toHaveLength(1)
expect(JSON.parse(messages[0]!)).toMatchObject({
ok: false,
error: { code: 'runtime_replaced' },
_meta: { runtimeId: 'test-runtime' }
})
expect(handler).not.toHaveBeenCalled()
})
it('returns error for unknown method via dispatchStreaming', async () => {
const messages: string[] = []
const dispatcher = new RpcDispatcher({
-2
View File
@@ -3415,7 +3415,6 @@ export type PreloadApi = {
params?: unknown
timeoutMs?: number
expectedEnvironmentPairingRevision?: number
expectedRuntimeId?: string
}) => Promise<RuntimeRpcResponse<unknown>>
subscribe: (
args: {
@@ -3424,7 +3423,6 @@ export type PreloadApi = {
params?: unknown
timeoutMs?: number
expectedEnvironmentPairingRevision?: number
expectedRuntimeId?: string
},
callbacks: {
onResponse: (response: RuntimeRpcResponse<unknown>) => void
-2
View File
@@ -4429,7 +4429,6 @@ const api = {
params?: unknown
timeoutMs?: number
expectedEnvironmentPairingRevision?: number
expectedRuntimeId?: string
}): Promise<RuntimeRpcResponse<unknown>> =>
ipcRenderer.invoke('runtimeEnvironments:call', args),
subscribe: async (
@@ -4439,7 +4438,6 @@ const api = {
params?: unknown
timeoutMs?: number
expectedEnvironmentPairingRevision?: number
expectedRuntimeId?: string
},
callbacks: {
onResponse: (response: RuntimeRpcResponse<unknown>) => void
@@ -68,12 +68,7 @@ describe('subscribeRuntimeEnvironmentFromPreload', () => {
const cleanupPromise = subscribeRuntimeEnvironmentFromPreload(
ipc,
{
selector: 'desk',
method: 'terminal.subscribe',
expectedEnvironmentPairingRevision: 7,
expectedRuntimeId: 'runtime-1'
},
{ selector: 'desk', method: 'terminal.subscribe' },
{ onResponse, onBinary },
() => 'sub-1'
)
@@ -85,8 +80,6 @@ describe('subscribeRuntimeEnvironmentFromPreload', () => {
expect(ipc.invoke).toHaveBeenCalledWith('runtimeEnvironments:subscribe', {
selector: 'desk',
method: 'terminal.subscribe',
expectedEnvironmentPairingRevision: 7,
expectedRuntimeId: 'runtime-1',
subscriptionId: 'sub-1'
})
@@ -5,8 +5,6 @@ type RuntimeEnvironmentSubscribeArgs = {
method: string
params?: unknown
timeoutMs?: number
expectedEnvironmentPairingRevision?: number
expectedRuntimeId?: string
}
type RuntimeEnvironmentSubscriptionCallbacks = {
@@ -9,7 +9,6 @@ import {
encodeTerminalStreamText
} from '../../../../shared/terminal-stream-protocol'
import { createTerminalSessionStateSaveFailureMessage } from '../../../../shared/terminal-session-state-save-failure'
import { TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
import {
TERMINAL_INPUT_CHUNK_MAX_BYTES,
TERMINAL_INPUT_MAX_BYTES
@@ -2536,10 +2535,7 @@ describe('createRemoteRuntimePtyTransport', () => {
result: {
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: [
'agent-session.host-authority.v1',
TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY
]
capabilities: ['agent-session.host-authority.v1']
},
_meta: { runtimeId: 'runtime-remote' }
}
@@ -2638,14 +2634,12 @@ describe('createRemoteRuntimePtyTransport', () => {
worktree: 'id:repo1::/remote/wt',
clientMutationId: expect.any(String),
command: 'claude',
env: { ORCA_TAB_ID: 'tab-1', ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
env: { ORCA_TAB_ID: 'tab-1' },
tabId: 'tab-1',
leafId: '11111111-1111-4111-8111-111111111111',
focus: false,
presentation: 'background'
},
expectedEnvironmentPairingRevision: undefined,
timeoutMs: 15_000
})
expect(runtimeSubscribe).toHaveBeenCalledWith(
@@ -2697,34 +2691,20 @@ describe('createRemoteRuntimePtyTransport', () => {
})
it('reports a host stable-pane adoption as reattach without fresh-spawn ownership', async () => {
runtimeCall.mockImplementation(async (args: { method?: string }) =>
args.method === 'status.get'
? {
id: 'rpc-status',
ok: true,
result: {
runtimeId: 'runtime-remote',
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: [TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY]
},
_meta: { runtimeId: 'runtime-remote' }
}
: {
id: 'rpc-create',
ok: true,
result: {
terminal: {
handle: 'term-original',
worktreeId: 'repo1::/remote/wt',
title: 'Original',
surface: 'background',
isReattach: true
}
},
_meta: { runtimeId: 'runtime-remote' }
}
)
runtimeCall.mockResolvedValue({
id: 'rpc-create',
ok: true,
result: {
terminal: {
handle: 'term-original',
worktreeId: 'repo1::/remote/wt',
title: 'Original',
surface: 'background',
isReattach: true
}
},
_meta: { runtimeId: 'runtime-remote' }
})
const onPtySpawn = vi.fn()
const onReattachDetermined = vi.fn()
const { createRemoteRuntimePtyTransport } = await import('./remote-runtime-pty-transport')
@@ -2852,7 +2832,6 @@ describe('createRemoteRuntimePtyTransport', () => {
},
presentation: 'background'
},
expectedEnvironmentPairingRevision: undefined,
timeoutMs: 15_000
})
expect(runtimeCall).not.toHaveBeenCalledWith(
@@ -2878,13 +2857,9 @@ describe('createRemoteRuntimePtyTransport', () => {
id: 'rpc-status',
ok: true,
result: {
runtimeId: 'runtime-remote',
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: [
'agent-session.host-authority.v1',
TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY
]
capabilities: ['agent-session.host-authority.v1']
}
}
)
@@ -2930,7 +2905,6 @@ describe('createRemoteRuntimePtyTransport', () => {
},
presentation: 'background'
},
expectedEnvironmentPairingRevision: undefined,
timeoutMs: 15_000
})
expect(runtimeCall).not.toHaveBeenCalledWith(
@@ -14,12 +14,7 @@ import {
TERMINAL_INPUT_MAX_BYTES
} from '../../../../shared/terminal-input'
import { CLIPBOARD_TEXT_MEASURE_YIELD_CODE_UNITS } from '../../../../shared/clipboard-text'
import {
MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
RUNTIME_PROTOCOL_VERSION,
TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY,
TERMINAL_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY
} from '../../../../shared/protocol-version'
import { TERMINAL_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
describe('createRemoteRuntimePtyTransport', () => {
const runtimeCall = vi.fn()
@@ -118,19 +113,6 @@ describe('createRemoteRuntimePtyTransport', () => {
}
}
function currentRuntimeStatus(capabilities: string[] = []): unknown {
return {
ok: true,
result: {
runtimeId: 'runtime-1',
graphStatus: 'ready',
runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION,
minCompatibleRuntimeClientVersion: MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
capabilities: [TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY, ...capabilities]
}
}
}
function emitOutput(streamId: number, data: string): void {
subscriptionCallbacks?.onBinary?.(
encodeTerminalStreamFrame({
@@ -205,18 +187,6 @@ describe('createRemoteRuntimePtyTransport', () => {
subscriptionSendBinary.mockReset()
refreshSessionTabsSnapshot.mockClear()
runtimeCall.mockImplementation(async (request: { method: string; params?: unknown }) => {
if (request.method === 'status.get') {
return {
ok: true,
result: {
runtimeId: 'runtime-1',
graphStatus: 'ready',
runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION,
minCompatibleRuntimeClientVersion: MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
capabilities: [TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY]
}
}
}
if (request.method === 'session.tabs.activate') {
const params = request.params as { tabId: string; leafId?: string }
const resolvedLeafId = params.leafId ?? 'pane:1'
@@ -571,7 +541,10 @@ describe('createRemoteRuntimePtyTransport', () => {
let createCalls = 0
runtimeCall.mockImplementation(async (args: { method: string; params?: unknown }) => {
if (args.method === 'status.get') {
return currentRuntimeStatus([TERMINAL_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY])
return {
ok: true,
result: { capabilities: [TERMINAL_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY] }
}
}
if (args.method === 'terminal.create') {
createCalls += 1
@@ -619,14 +592,13 @@ describe('createRemoteRuntimePtyTransport', () => {
try {
const startedAt = Date.now()
let createCalls = 0
let statusCalls = 0
runtimeCall.mockImplementation(async (args: { method: string }) => {
if (args.method === 'status.get') {
statusCalls += 1
if (statusCalls > 1) {
vi.setSystemTime(startedAt + 59_000)
vi.setSystemTime(startedAt + 59_000)
return {
ok: true,
result: { capabilities: [TERMINAL_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY] }
}
return currentRuntimeStatus([TERMINAL_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY])
}
if (args.method === 'terminal.create') {
createCalls += 1
@@ -660,7 +632,7 @@ describe('createRemoteRuntimePtyTransport', () => {
it('does not retry an unknown create outcome against an older runtime', async () => {
runtimeCall.mockImplementation(async (args: { method: string }) => {
if (args.method === 'status.get') {
return currentRuntimeStatus()
return { ok: true, result: { capabilities: [] } }
}
throw Object.assign(new Error('Timed out waiting for the remote Orca runtime.'), {
code: 'runtime_timeout'
@@ -681,16 +653,12 @@ describe('createRemoteRuntimePtyTransport', () => {
})
it('surfaces an authoritative capability-probe failure after an unknown create outcome', async () => {
let statusCalls = 0
runtimeCall.mockImplementation(async (args: { method: string }) => {
if (args.method === 'status.get' && ++statusCalls > 1) {
if (args.method === 'status.get') {
throw Object.assign(new Error('Remote runtime pairing credentials expired.'), {
code: 'unauthorized'
})
}
if (args.method === 'status.get') {
return currentRuntimeStatus([TERMINAL_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY])
}
throw Object.assign(new Error('Timed out waiting for the remote Orca runtime.'), {
code: 'runtime_timeout'
})
@@ -726,7 +694,10 @@ describe('createRemoteRuntimePtyTransport', () => {
}, args.timeoutMs)
})
}
return currentRuntimeStatus([TERMINAL_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY])
return {
ok: true,
result: { capabilities: [TERMINAL_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY] }
}
}
if (args.method === 'terminal.create' && reachable) {
return { ok: true, result: { terminal: { handle: 'terminal-recovered' } } }
@@ -792,10 +763,9 @@ describe('createRemoteRuntimePtyTransport', () => {
return {
ok: true,
result: {
runtimeId: 'runtime-1',
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: ['agent-session.host-authority.v1', 'terminal.attribution-removed.v1']
capabilities: ['agent-session.host-authority.v1']
}
}
}
@@ -843,65 +813,9 @@ describe('createRemoteRuntimePtyTransport', () => {
.filter((args) => args.method === 'terminal.createAgentSession')
expect(allCreates.every((args) => args.params?.clientOperationId === operationId)).toBe(true)
expect(runtimeCall.mock.calls.some(([args]) => args.method === 'terminal.create')).toBe(false)
expect(
runtimeCall.mock.calls.filter(([args]) => args.method === 'status.get').length
).toBeGreaterThan(1)
transport.destroy?.()
} finally {
vi.useRealTimers()
}
})
it('refuses structured replay after an in-place host downgrade', async () => {
vi.useFakeTimers()
try {
let statusCalls = 0
const onError = vi.fn()
runtimeCall.mockImplementation(async (args: { method: string }) => {
if (args.method === 'status.get') {
statusCalls += 1
return {
ok: true,
result: {
runtimeId: 'runtime-1',
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities:
statusCalls === 1
? ['agent-session.host-authority.v1', 'terminal.attribution-removed.v1']
: ['agent-session.host-authority.v1']
}
}
}
if (args.method === 'terminal.createAgentSession') {
throw Object.assign(new Error('Timed out waiting for the remote Orca runtime.'), {
code: 'runtime_timeout'
})
}
return { ok: true, result: {} }
})
const { createRemoteRuntimePtyTransport } = await import('./remote-runtime-pty-transport')
const transport = createRemoteRuntimePtyTransport('env-1', {
worktreeId: 'wt-1',
tabId: 'tab-1',
leafId: 'pane:1',
launchAgent: 'codex'
})
const connect = transport.connect({ url: '', callbacks: { onError } })
await vi.advanceTimersByTimeAsync(250)
await connect
expect(onError).toHaveBeenCalledWith(
expect.stringContaining('no fallback launch was attempted')
)
expect(
runtimeCall.mock.calls.filter(([args]) => args.method === 'terminal.createAgentSession')
).toHaveLength(1)
expect(runtimeCall.mock.calls.filter(([args]) => args.method === 'status.get')).toHaveLength(
2
1
)
expect(runtimeCall.mock.calls.some(([args]) => args.method === 'terminal.create')).toBe(false)
transport.destroy?.()
} finally {
vi.useRealTimers()
@@ -3941,9 +3855,6 @@ describe('createRemoteRuntimePtyTransport', () => {
it('closes a remote terminal created after the pane was destroyed', async () => {
let resolveCreate: (value: unknown) => void = () => {}
runtimeCall.mockImplementation((args) => {
if (args.method === 'status.get') {
return Promise.resolve(currentRuntimeStatus())
}
if (args.method === 'terminal.create') {
return new Promise((resolve) => {
resolveCreate = resolve
@@ -3959,11 +3870,6 @@ describe('createRemoteRuntimePtyTransport', () => {
})
const connect = transport.connect({ url: '', callbacks: {} })
await vi.waitFor(() =>
expect(runtimeCall).toHaveBeenCalledWith(
expect.objectContaining({ method: 'terminal.create' })
)
)
transport.destroy?.()
resolveCreate({ ok: true, result: { terminal: { handle: 'terminal-late' } } })
await connect
@@ -3972,17 +3878,13 @@ describe('createRemoteRuntimePtyTransport', () => {
selector: 'env-1',
method: 'terminal.close',
params: { terminal: 'terminal-late' },
timeoutMs: 15_000,
expectedEnvironmentPairingRevision: undefined
timeoutMs: 15_000
})
})
it('cannot let a stale create completion replace a newer attached terminal', async () => {
let resolveCreate: (value: unknown) => void = () => {}
runtimeCall.mockImplementation((args) => {
if (args.method === 'status.get') {
return Promise.resolve(currentRuntimeStatus())
}
if (args.method === 'terminal.create') {
return new Promise((resolve) => {
resolveCreate = resolve
@@ -3998,11 +3900,6 @@ describe('createRemoteRuntimePtyTransport', () => {
})
const connect = transport.connect({ url: '', callbacks: {} })
await vi.waitFor(() =>
expect(runtimeCall).toHaveBeenCalledWith(
expect.objectContaining({ method: 'terminal.create' })
)
)
transport.attach({ existingPtyId: 'remote:env-2@@terminal-attached', callbacks: {} })
resolveCreate({ ok: true, result: { terminal: { handle: 'terminal-late' } } })
await connect
@@ -4013,8 +3910,7 @@ describe('createRemoteRuntimePtyTransport', () => {
selector: 'env-1',
method: 'terminal.close',
params: { terminal: 'terminal-late' },
timeoutMs: 15_000,
expectedEnvironmentPairingRevision: undefined
timeoutMs: 15_000
})
transport.destroy?.()
})
@@ -4028,7 +3924,7 @@ describe('createRemoteRuntimePtyTransport', () => {
result: {
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: ['agent-session.host-authority.v1', 'terminal.attribution-removed.v1']
capabilities: ['agent-session.host-authority.v1']
}
})
}
@@ -4078,7 +3974,7 @@ describe('createRemoteRuntimePtyTransport', () => {
result: {
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: ['agent-session.host-authority.v1', 'terminal.attribution-removed.v1']
capabilities: ['agent-session.host-authority.v1']
}
})
}
@@ -4193,7 +4089,7 @@ describe('createRemoteRuntimePtyTransport', () => {
method: 'terminal.create',
params: expect.objectContaining({
command: "codex 'linked issue context'",
envToDelete: ['CODEX_HOME', 'ORCA_CODEX_HOME', 'ORCA_ENABLE_GIT_ATTRIBUTION'],
envToDelete: ['CODEX_HOME', 'ORCA_CODEX_HOME'],
startupCommandDelivery: 'shell-ready',
terminalColorQueryReplies: { foreground: '#ffffff', background: '#282c34' }
})
@@ -4209,11 +4105,7 @@ describe('createRemoteRuntimePtyTransport', () => {
result: {
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: [
'agent-session.host-authority.v1',
'agent-session.omp-resume-path.v1',
'terminal.attribution-removed.v1'
]
capabilities: ['agent-session.host-authority.v1', 'agent-session.omp-resume-path.v1']
}
}
: { ok: true, result: { terminal: { handle: 'terminal-1' } } }
@@ -4277,7 +4169,7 @@ describe('createRemoteRuntimePtyTransport', () => {
result: {
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: ['agent-session.host-authority.v1', 'terminal.attribution-removed.v1']
capabilities: ['agent-session.host-authority.v1']
}
}
: {
@@ -4327,10 +4219,9 @@ describe('createRemoteRuntimePtyTransport', () => {
? {
ok: true,
result: {
runtimeId: 'runtime-1',
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: [TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY]
capabilities: []
}
}
: { ok: true, result: { terminal: { handle: 'terminal-legacy' } } }
@@ -4366,12 +4257,7 @@ describe('createRemoteRuntimePtyTransport', () => {
worktree: 'id:wt-1',
clientMutationId: expect.any(String),
command: "codex '--model' 'gpt-5' 'resume' 'session-1'",
env: {
CODEX_PROFILE: 'captured',
ORCA_AGENT_LAUNCH_TOKEN: 'fresh-token',
ORCA_ATTRIBUTION_BYPASS: '1'
},
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
env: { CODEX_PROFILE: 'captured', ORCA_AGENT_LAUNCH_TOKEN: 'fresh-token' },
launchConfig: {
agentArgs: '--model gpt-5',
agentEnv: { CODEX_PROFILE: 'captured' }
@@ -4383,9 +4269,7 @@ describe('createRemoteRuntimePtyTransport', () => {
focus: false,
presentation: 'background'
},
timeoutMs: 15_000,
expectedEnvironmentPairingRevision: undefined,
expectedRuntimeId: 'runtime-1'
timeoutMs: 15_000
})
expect(runtimeCall).not.toHaveBeenCalledWith(
expect.objectContaining({ method: 'terminal.createAgentSession' })
@@ -5802,9 +5686,6 @@ describe('createRemoteRuntimePtyTransport', () => {
it('returns runtime acceptance for acknowledged terminal input', async () => {
runtimeCall.mockImplementation((args) => {
if (args.method === 'status.get') {
return Promise.resolve(currentRuntimeStatus())
}
if (args.method === 'terminal.create') {
return Promise.resolve({ ok: true, result: { terminal: { handle: 'terminal-1' } } })
}
@@ -5844,9 +5725,6 @@ describe('createRemoteRuntimePtyTransport', () => {
vi.useFakeTimers()
try {
runtimeCall.mockImplementation((args) => {
if (args.method === 'status.get') {
return Promise.resolve(currentRuntimeStatus())
}
if (args.method === 'terminal.create') {
return Promise.resolve({ ok: true, result: { terminal: { handle: 'terminal-1' } } })
}
@@ -5892,9 +5770,6 @@ describe('createRemoteRuntimePtyTransport', () => {
it('returns false when acknowledged terminal input is rejected by the runtime', async () => {
runtimeCall.mockImplementation((args) => {
if (args.method === 'status.get') {
return Promise.resolve(currentRuntimeStatus())
}
if (args.method === 'terminal.create') {
return Promise.resolve({ ok: true, result: { terminal: { handle: 'terminal-1' } } })
}
@@ -5920,9 +5795,6 @@ describe('createRemoteRuntimePtyTransport', () => {
it('splits large acknowledged remote input before terminal.send RPCs', async () => {
runtimeCall.mockImplementation((args) => {
if (args.method === 'status.get') {
return Promise.resolve(currentRuntimeStatus())
}
if (args.method === 'terminal.create') {
return Promise.resolve({ ok: true, result: { terminal: { handle: 'terminal-1' } } })
}
@@ -5962,9 +5834,6 @@ describe('createRemoteRuntimePtyTransport', () => {
vi.useFakeTimers()
try {
runtimeCall.mockImplementation((args) => {
if (args.method === 'status.get') {
return Promise.resolve(currentRuntimeStatus())
}
if (args.method === 'terminal.create') {
return Promise.resolve({ ok: true, result: { terminal: { handle: 'terminal-1' } } })
}
@@ -6014,9 +5883,6 @@ describe('createRemoteRuntimePtyTransport', () => {
const firstChunk = 'x'.repeat(TERMINAL_INPUT_CHUNK_MAX_BYTES)
const rejectedChunk = `tail${'y'.repeat(TERMINAL_INPUT_CHUNK_MAX_BYTES - 4)}`
runtimeCall.mockImplementation((args) => {
if (args.method === 'status.get') {
return Promise.resolve(currentRuntimeStatus())
}
if (args.method === 'terminal.create') {
return Promise.resolve({ ok: true, result: { terminal: { handle: 'terminal-1' } } })
}
@@ -6055,9 +5921,6 @@ describe('createRemoteRuntimePtyTransport', () => {
it('rejects oversized acknowledged remote input before runtime RPCs', async () => {
runtimeCall.mockImplementation((args) => {
if (args.method === 'status.get') {
return Promise.resolve(currentRuntimeStatus())
}
if (args.method === 'terminal.create') {
return Promise.resolve({ ok: true, result: { terminal: { handle: 'terminal-1' } } })
}
@@ -20,11 +20,8 @@ import type {
RuntimeTerminalSend
} from '../../../../shared/runtime-types'
import {
AGENT_SESSION_HOST_AUTHORITY_RUNTIME_CAPABILITY,
AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY,
TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY,
TERMINAL_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY,
type RuntimeCapability
TERMINAL_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY
} from '../../../../shared/protocol-version'
import {
isTerminalInputTooLargeWithDeferredMeasurement,
@@ -37,11 +34,7 @@ import type {
PtyTransportRecoveryState
} from './pty-transport-types'
import { createPtyOutputProcessor } from './pty-transport'
import {
RuntimeRpcCallError,
unwrapRuntimeRpcResult,
type LiveRuntimeEnvironmentAuthority
} from '../../runtime/runtime-rpc-client'
import { RuntimeRpcCallError, unwrapRuntimeRpcResult } from '../../runtime/runtime-rpc-client'
import {
getRemoteRuntimePtyEnvironmentId,
getRemoteRuntimeTerminalHandle,
@@ -90,10 +83,6 @@ import {
ptyShutdownLifecycleHandlers
} from './pty-shutdown-data-suspension'
import { getRuntimeEnvironmentRevision } from '@/runtime/runtime-environment-revision'
import {
addLegacyTerminalAttributionDisableRequest,
withLegacyTerminalAttributionDisabledEnv
} from '../../../../shared/legacy-terminal-attribution-env'
const REMOTE_TERMINAL_INPUT_FLUSH_MS = 8
const REMOTE_TERMINAL_VIEWPORT_FLUSH_MS = 33
@@ -103,8 +92,6 @@ const HOST_SESSION_ATTACH_TIMEOUT_MS = 15_000
const HOST_SESSION_INVENTORY_MAX_WINDOWS_PER_RECOVERY = 2
const HOST_SESSION_SAME_HANDLE_END_REUSE_LIMIT = 2
const TERMINAL_CREATE_RETRY_DELAYS_MS = [250, 500, 1000, 2000, 4000, 8000, 15_000, 30_000] as const
const AGENT_SESSION_REPLAY_UPDATE_REQUIRED_MESSAGE =
'Remote agent recovery requires the same updated Orca server that accepted the launch. Update the workspace host and try again; no fallback launch was attempted.'
type HostHandleReplacementPolicy = 'reuse' | 'prefer-replacement' | 'require-replacement'
@@ -371,7 +358,6 @@ export function createRemoteRuntimePtyTransport(
// Why: reconnect retries must replay one host operation instead of creating
// another fresh agent when the first response was lost.
const agentCreateOperation = createAgentSessionCreateOperation()
let agentSessionCreateAuthority: LiveRuntimeEnvironmentAuthority | null = null
const outputProcessor = createPtyOutputProcessor({
onTitleChange,
onBell,
@@ -866,16 +852,14 @@ export function createRemoteRuntimePtyTransport(
environmentId: string,
method: string,
params?: unknown,
timeoutMs = 15_000,
expectedRuntimeId?: string
timeoutMs = 15_000
): Promise<TResult> {
const response = await window.api.runtimeEnvironments.call({
selector: environmentId,
method,
params,
timeoutMs,
expectedEnvironmentPairingRevision: runtimeEnvironmentPairingRevision,
...(expectedRuntimeId ? { expectedRuntimeId } : {})
expectedEnvironmentPairingRevision: runtimeEnvironmentPairingRevision
})
return unwrapRuntimeRpcResult(response as RuntimeRpcResponse<TResult>)
}
@@ -924,8 +908,7 @@ export function createRemoteRuntimePtyTransport(
reconcileExisting: boolean
) => Promise<RemoteAgentSessionLaunchResult>,
environmentId: string,
expectedLifecycleEpoch: number,
beforeReplay?: (timeoutMs: number) => Promise<void>
expectedLifecycleEpoch: number
): Promise<RemoteAgentSessionLaunchResult | null> {
let retryAttempt = 0
// Structured operations already carry their replay proof; ordinary terminal.create
@@ -1010,9 +993,6 @@ export function createRemoteRuntimePtyTransport(
break
}
try {
if (reconcileExisting) {
await beforeReplay?.(Math.min(5_000, createRemainingMs ?? 5_000))
}
return await invoke(Math.min(15_000, createRemainingMs ?? 15_000), reconcileExisting)
} catch (error) {
lastError = error
@@ -1995,10 +1975,8 @@ export function createRemoteRuntimePtyTransport(
const commandToSend = options.command ?? command
const startupCommandDeliveryToSend =
options.startupCommandDelivery ?? startupCommandDelivery
const envToSend = withLegacyTerminalAttributionDisabledEnv(options.env ?? env)
const envToDeleteToSend = addLegacyTerminalAttributionDisableRequest(
options.envToDelete ?? envToDelete
)
const envToSend = options.env ?? env
const envToDeleteToSend = options.envToDelete ?? envToDelete
const launchConfigToSend = options.launchConfig ?? launchConfig
const resumeProviderSessionToSend = options.resumeProviderSession ?? resumeProviderSession
const launchTokenToSend = options.launchToken ?? launchToken
@@ -2026,7 +2004,7 @@ export function createRemoteRuntimePtyTransport(
presentation: 'background' as const,
...(activate === true ? { activate: true } : {})
}
const legacyCreate = ({ authority }: { authority: LiveRuntimeEnvironmentAuthority }) =>
const legacyCreate = () =>
createWithUnknownOutcomeRecovery(
'terminal',
(timeoutMs, reconcileExisting) =>
@@ -2037,49 +2015,13 @@ export function createRemoteRuntimePtyTransport(
...legacyCreateParams,
...(reconcileExisting ? { reconcileExisting: true } : {})
},
timeoutMs,
authority.runtimeId
timeoutMs
),
createEnvironmentId,
connectLifecycleEpoch
)
const requiredAgentSessionCapabilities: readonly RuntimeCapability[] =
resumeProviderSessionToSend && launchAgentToSend === 'omp'
? [AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY]
: []
const revalidateAgentSessionReplay = async (
timeoutMs: number,
authority: LiveRuntimeEnvironmentAuthority
): Promise<void> => {
const status = await callRuntimeForEnvironment<RuntimeStatus>(
createEnvironmentId,
'status.get',
undefined,
timeoutMs,
authority.runtimeId
)
const requiredCapabilities = [
AGENT_SESSION_HOST_AUTHORITY_RUNTIME_CAPABILITY,
TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY,
...requiredAgentSessionCapabilities
]
const capabilities = Array.isArray(status.capabilities) ? status.capabilities : []
if (
status.runtimeId !== authority.runtimeId ||
requiredCapabilities.some((capability) => !capabilities.includes(capability))
) {
throw new Error(AGENT_SESSION_REPLAY_UPDATE_REQUIRED_MESSAGE)
}
}
const hostAuthorityCreate = (authority: LiveRuntimeEnvironmentAuthority) => {
if (
agentSessionCreateAuthority &&
agentSessionCreateAuthority.runtimeId !== authority.runtimeId
) {
throw new Error(AGENT_SESSION_REPLAY_UPDATE_REQUIRED_MESSAGE)
}
agentSessionCreateAuthority ??= authority
return createWithUnknownOutcomeRecovery(
const hostAuthorityCreate = () =>
createWithUnknownOutcomeRecovery(
'agent-session',
(timeoutMs) =>
resumeProviderSessionToSend
@@ -2101,8 +2043,7 @@ export function createRemoteRuntimePtyTransport(
placement: { tabId, leafId },
presentation: 'background'
},
timeoutMs,
authority.runtimeId
timeoutMs
)
: callRuntimeForEnvironment<RuntimeCreateAgentSessionResult>(
createEnvironmentId,
@@ -2124,37 +2065,23 @@ export function createRemoteRuntimePtyTransport(
},
agentCreateOperation.clientOperationId
),
timeoutMs,
authority.runtimeId
timeoutMs
),
createEnvironmentId,
connectLifecycleEpoch,
(timeoutMs) => revalidateAgentSessionReplay(timeoutMs, authority)
connectLifecycleEpoch
)
}
const created = launchAgentToSend
? agentSessionRequiresHostAuthorityReplay
? agentSessionCreateAuthority
? await hostAuthorityCreate(agentSessionCreateAuthority)
: await runRemoteAgentSessionLaunch<RemoteAgentSessionLaunchResult | null>({
environmentId: createEnvironmentId,
expectedEnvironmentPairingRevision: runtimeEnvironmentPairingRevision,
hostAuthority: hostAuthorityCreate,
requiredHostAuthorityCapabilities: requiredAgentSessionCapabilities,
legacy: legacyCreate
})
? await hostAuthorityCreate()
: await runRemoteAgentSessionLaunch<RemoteAgentSessionLaunchResult | null>({
environmentId: createEnvironmentId,
expectedEnvironmentPairingRevision: runtimeEnvironmentPairingRevision,
hostAuthority: hostAuthorityCreate,
requiredHostAuthorityCapabilities: requiredAgentSessionCapabilities,
...(resumeProviderSessionToSend && launchAgentToSend === 'omp'
? { hostAuthorityCapability: AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY }
: {}),
legacy: legacyCreate
})
: await runRemoteAgentSessionLaunch<RemoteAgentSessionLaunchResult | null>({
environmentId: createEnvironmentId,
expectedEnvironmentPairingRevision: runtimeEnvironmentPairingRevision,
legacy: legacyCreate
})
: await legacyCreate()
if (!created) {
if (!destroyed && lifecycleEpoch === connectLifecycleEpoch) {
connecting = false
@@ -2230,21 +2157,13 @@ export function createRemoteRuntimePtyTransport(
if (!destroyed && lifecycleEpoch === connectLifecycleEpoch) {
connecting = false
const message = runtimeTerminalErrorMessage(error)
const recoverable = isRecoverableRemoteRuntimeConnectionError(
toRemoteRuntimeClientErrorLike(error)
)
if (isRemoteTerminalGoneMessage(message)) {
recovery.cancel()
handleRemoteTerminalError(error)
} else if (
recoverable ||
terminalCreateNeedsReconciliation ||
agentSessionRequiresHostAuthorityReplay
isRecoverableRemoteRuntimeConnectionError(toRemoteRuntimeClientErrorLike(error))
) {
recovery.markDisconnected()
if (!recoverable) {
surfaceErrorMessage(message)
}
} else {
recovery.cancel()
emitRecoveryState()
@@ -378,8 +378,6 @@ describe('launchAgentBackgroundSession remote runtime and SSH startup delivery',
)
expect(mockRuntimeEnvironmentCall).toHaveBeenCalledWith({
selector: 'env-1',
expectedEnvironmentPairingRevision: undefined,
expectedRuntimeId: 'remote-runtime',
method: 'terminal.createAgentSession',
params: expect.objectContaining({
clientOperationId: expect.stringMatching(/^\d{13}-[0-9a-f]{32}$/),
@@ -422,7 +420,7 @@ describe('launchAgentBackgroundSession remote runtime and SSH startup delivery',
graphStatus: 'ready',
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: ['terminal.attribution-removed.v1']
capabilities: []
}
})
}
@@ -444,7 +442,6 @@ describe('launchAgentBackgroundSession remote runtime and SSH startup delivery',
expect(mockRuntimeEnvironmentTransportCall).toHaveBeenCalledWith(
expect.objectContaining({
expectedRuntimeId: 'old-runtime',
method: 'terminal.create',
params: expect.objectContaining({
worktree: 'id:wt-1',
@@ -3,11 +3,6 @@ import type { StartupCommandDelivery } from '../../../shared/codex-startup-deliv
import type { SessionOptionValue } from '../../../shared/native-chat-session-options'
import type { RuntimeTerminalCreate } from '../../../shared/runtime-types'
import type { TuiAgent } from '../../../shared/types'
import { TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY } from '../../../shared/protocol-version'
import {
addLegacyTerminalAttributionDisableRequest,
withLegacyTerminalAttributionDisabledEnv
} from '../../../shared/legacy-terminal-attribution-env'
import {
createAgentSessionCreateOperation,
toAgentLaunchPreferences,
@@ -38,8 +33,7 @@ export async function createRuntimeAgentBackgroundTerminal(args: {
const launchPreferences = toAgentLaunchPreferences(args.sessionOptions)
return await runRemoteAgentSessionLaunch({
environmentId: args.environmentId,
requiredHostAuthorityCapabilities: [TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY],
hostAuthority: (authority) =>
hostAuthority: () =>
operation.run((clientOperationId) =>
callRuntimeRpc<{ terminal: RuntimeTerminalCreate }>(
{ kind: 'environment', environmentId: args.environmentId },
@@ -58,14 +52,10 @@ export async function createRuntimeAgentBackgroundTerminal(args: {
},
clientOperationId
),
{
timeoutMs: 15_000,
expectedEnvironmentPairingRevision: authority.expectedEnvironmentPairingRevision,
expectedRuntimeId: authority.runtimeId
}
{ timeoutMs: 15_000 }
)
),
legacy: ({ skipCompatibilityCheck, authority }) =>
legacy: ({ skipCompatibilityCheck }) =>
callRuntimeRpc<{ terminal: RuntimeTerminalCreate }>(
{ kind: 'environment', environmentId: args.environmentId },
'terminal.create',
@@ -75,8 +65,7 @@ export async function createRuntimeAgentBackgroundTerminal(args: {
...(args.legacy.startupCommandDelivery
? { startupCommandDelivery: args.legacy.startupCommandDelivery }
: {}),
env: withLegacyTerminalAttributionDisabledEnv(args.legacy.env),
envToDelete: addLegacyTerminalAttributionDisableRequest(undefined),
env: args.legacy.env,
launchConfig: args.legacy.launchConfig,
launchToken: args.legacy.launchToken,
launchAgent: args.agent,
@@ -85,12 +74,7 @@ export async function createRuntimeAgentBackgroundTerminal(args: {
leafId: args.leafId,
presentation: 'background'
},
{
timeoutMs: 15_000,
skipCompatibilityCheck,
expectedEnvironmentPairingRevision: authority.expectedEnvironmentPairingRevision,
expectedRuntimeId: authority.runtimeId
}
{ timeoutMs: 15_000, skipCompatibilityCheck }
)
})
}
@@ -1,6 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { getDefaultSettings } from '../../../shared/constants'
import { TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY } from '../../../shared/protocol-version'
import { useAppStore } from '@/store'
import {
activateAndRevealWorktree,
@@ -24,35 +23,6 @@ function makeWebRuntimeWorktree() {
}
}
function makeWakeTerminalRuntimeCall(worktreeId: string) {
return vi.fn(async (args: { method?: string }) => {
if (args.method === 'status.get') {
return {
ok: true,
result: { capabilities: [TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY] }
}
}
if (args.method === 'session.tabs.list') {
return {
ok: true,
result: {
worktree: worktreeId,
publicationEpoch: 'epoch-1',
snapshotVersion: 1,
activeGroupId: null,
activeTabId: null,
activeTabType: null,
tabs: []
}
}
}
return {
ok: true,
result: { tab: { id: 'host-tab-1::leaf-1', leafId: 'leaf-1' } }
}
})
}
/** Activates and asserts a focusable tab appeared with no queued startup, returning its id. */
function activateAndExpectNoRelaunch(
worktreeId: string,
@@ -566,7 +536,16 @@ describe('activateAndRevealWorktree', () => {
it('respawns a host terminal when waking a slept web workspace with dead local PTYs', async () => {
const worktree = makeWebRuntimeWorktree()
const callRuntimeEnvironment = makeWakeTerminalRuntimeCall(worktree.id)
const callRuntimeEnvironment = vi
.fn()
.mockResolvedValueOnce({
ok: true,
result: { repoId: worktree.repoId, worktreeId: worktree.id, activated: true }
})
.mockResolvedValueOnce({
ok: true,
result: { tabId: 'host-tab-1', terminal: 'term_host' }
})
;(globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ = true
vi.stubGlobal('window', {
api: {
@@ -633,19 +612,9 @@ describe('activateAndRevealWorktree', () => {
})
ensureWebRuntimeWorktreeTerminalAfterWake(worktree.id)
await vi.waitFor(() => {
expect(callRuntimeEnvironment).toHaveBeenCalledTimes(3)
})
await new Promise((resolve) => setTimeout(resolve, 0))
expect(callRuntimeEnvironment).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
selector: 'web-runtime-1',
method: 'status.get'
})
)
expect(callRuntimeEnvironment).toHaveBeenNthCalledWith(
2,
expect(callRuntimeEnvironment).toHaveBeenCalledWith(
expect.objectContaining({
selector: 'web-runtime-1',
method: 'session.tabs.createTerminal'
@@ -655,7 +624,10 @@ describe('activateAndRevealWorktree', () => {
it('respawns wake terminals on the explicit owner runtime when focus changed', async () => {
const worktree = makeWorktree()
const callRuntimeEnvironment = makeWakeTerminalRuntimeCall(worktree.id)
const callRuntimeEnvironment = vi.fn().mockResolvedValue({
ok: true,
result: { tabId: 'host-tab-1', terminal: 'term_host' }
})
;(globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ = true
vi.stubGlobal('window', {
api: {
@@ -704,19 +676,9 @@ describe('activateAndRevealWorktree', () => {
})
ensureWebRuntimeWorktreeTerminalAfterWake(worktree.id)
await vi.waitFor(() => {
expect(callRuntimeEnvironment).toHaveBeenCalledTimes(3)
})
await new Promise((resolve) => setTimeout(resolve, 0))
expect(callRuntimeEnvironment).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
selector: 'owner-runtime',
method: 'status.get'
})
)
expect(callRuntimeEnvironment).toHaveBeenNthCalledWith(
2,
expect(callRuntimeEnvironment).toHaveBeenCalledWith(
expect.objectContaining({
selector: 'owner-runtime',
method: 'session.tabs.createTerminal'
@@ -1,7 +1,6 @@
import path from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { getDefaultSettings } from '../../../shared/constants'
import { TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY } from '../../../shared/protocol-version'
import type { Worktree } from '../../../shared/types'
import { resetWebRuntimeWakeTerminalRespawnForTests } from '@/runtime/web-runtime-wake-terminal-respawn'
import { resetWebSessionTabsSnapshotFreshnessForTests } from '@/runtime/web-session-tabs-sync'
@@ -49,43 +48,21 @@ function makeWorktree(): Worktree {
describe('empty remote worktree activation', () => {
it('creates a host terminal when waking an empty remote workspace', async () => {
const worktree = makeWorktree()
const callRuntimeEnvironment = vi.fn(async (args: { method?: string }) => {
if (args.method === 'status.get') {
return {
ok: true,
result: { capabilities: [TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY] }
}
}
if (args.method === 'session.tabs.list') {
return {
ok: true,
result: {
worktree: worktree.id,
publicationEpoch: 'epoch-1',
snapshotVersion: 1,
activeGroupId: null,
activeTabId: null,
activeTabType: null,
tabs: []
}
}
}
return {
ok: true,
result: {
tab: {
type: 'terminal',
id: 'host-tab-1::leaf-1',
parentTabId: 'host-tab-1',
leafId: 'leaf-1',
title: 'Terminal 1',
terminal: 'term_host',
status: 'ready',
isActive: true
},
publicationEpoch: 'epoch-1',
snapshotVersion: 1
}
const callRuntimeEnvironment = vi.fn().mockResolvedValueOnce({
ok: true,
result: {
tab: {
type: 'terminal',
id: 'host-tab-1::leaf-1',
parentTabId: 'host-tab-1',
leafId: 'leaf-1',
title: 'Terminal 1',
terminal: 'term_host',
status: 'ready',
isActive: true
},
publicationEpoch: 'epoch-1',
snapshotVersion: 1
}
})
;(globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ = true
@@ -123,18 +100,10 @@ describe('empty remote worktree activation', () => {
ensureWebRuntimeWorktreeTerminalAfterWake(worktree.id)
await vi.waitFor(() => {
expect(callRuntimeEnvironment).toHaveBeenCalledTimes(3)
expect(callRuntimeEnvironment).toHaveBeenCalled()
})
expect(callRuntimeEnvironment).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
selector: 'web-runtime-1',
method: 'status.get'
})
)
expect(callRuntimeEnvironment).toHaveBeenNthCalledWith(
2,
expect(callRuntimeEnvironment).toHaveBeenCalledWith(
expect.objectContaining({
selector: 'web-runtime-1',
method: 'session.tabs.createTerminal',
@@ -12,8 +12,7 @@ export async function callAbortableRuntimeEnvironment(
params: unknown,
timeoutMs: number | undefined,
signal: AbortSignal,
expectedEnvironmentPairingRevision?: number,
expectedRuntimeId?: string
expectedEnvironmentPairingRevision?: number
): Promise<RuntimeRpcResponse<unknown>> {
if (signal.aborted) {
throw createRuntimeRpcAbortError()
@@ -48,14 +47,7 @@ export async function callAbortableRuntimeEnvironment(
signal.addEventListener('abort', onAbort, { once: true })
void window.api.runtimeEnvironments
.subscribe(
{
selector: environmentId,
method,
params,
timeoutMs,
expectedEnvironmentPairingRevision,
expectedRuntimeId
},
{ selector: environmentId, method, params, timeoutMs, expectedEnvironmentPairingRevision },
{
onResponse: (response) => finish(() => resolve(response)),
onError: (error) => finish(() => reject(new Error(error.message))),
@@ -1,6 +1,8 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({ probe: vi.fn() }))
const mocks = vi.hoisted(() => ({
supportsCapability: vi.fn()
}))
vi.mock('./runtime-rpc-client', () => ({
RuntimeRpcCallError: class RuntimeRpcCallError extends Error {
@@ -10,110 +12,75 @@ vi.mock('./runtime-rpc-client', () => ({
this.code = response.error.code
}
},
probeLiveRuntimeEnvironmentCapabilities: mocks.probe
runtimeEnvironmentSupportsCapability: mocks.supportsCapability
}))
import { RuntimeRpcCallError } from './runtime-rpc-client'
import { runRemoteAgentSessionLaunch } from './remote-agent-session-launch'
const authority = {
runtimeId: 'runtime-1',
expectedEnvironmentPairingRevision: 7,
capabilities: [
'terminal.attribution-removed.v1',
'agent-session.host-authority.v1',
'agent-session.omp-resume-path.v1'
]
} as const
describe('remote agent-session launch routing', () => {
beforeEach(() => {
mocks.probe.mockReset()
mocks.probe.mockResolvedValue({ supported: true, authority })
mocks.supportsCapability.mockReset()
})
it('uses one live all-capability probe before host authority', async () => {
it('uses host authority only when the host advertises it', async () => {
const hostAuthority = vi.fn().mockResolvedValue('structured')
const legacy = vi.fn().mockResolvedValue('legacy')
mocks.supportsCapability.mockResolvedValue(true)
await expect(
runRemoteAgentSessionLaunch({
environmentId: 'env-1',
expectedEnvironmentPairingRevision: 7,
hostAuthority,
requiredHostAuthorityCapabilities: ['agent-session.omp-resume-path.v1'],
hostAuthorityCapability: 'agent-session.omp-resume-path.v1',
legacy
})
).resolves.toBe('structured')
expect(mocks.probe).toHaveBeenCalledWith({
environmentId: 'env-1',
requiredCapabilities: [
'terminal.attribution-removed.v1',
'agent-session.host-authority.v1',
'agent-session.omp-resume-path.v1'
],
timeoutMs: 15_000,
expectedEnvironmentPairingRevision: 7
})
expect(hostAuthority).toHaveBeenCalledWith(authority)
expect(mocks.supportsCapability).toHaveBeenCalledWith(
'env-1',
'agent-session.omp-resume-path.v1'
)
expect(hostAuthority).toHaveBeenCalledOnce()
expect(legacy).not.toHaveBeenCalled()
})
it('uses fenced legacy when a structured-only capability is absent', async () => {
mocks.probe.mockResolvedValue({ supported: false, authority })
it('preserves the exact legacy path when the capability is absent', async () => {
const hostAuthority = vi.fn().mockResolvedValue('structured')
const legacy = vi.fn().mockResolvedValue('legacy')
mocks.supportsCapability.mockResolvedValue(false)
await expect(
runRemoteAgentSessionLaunch({ environmentId: 'env-1', hostAuthority, legacy })
).resolves.toBe('legacy')
expect(legacy).toHaveBeenCalledOnce()
expect(hostAuthority).not.toHaveBeenCalled()
})
it('keeps legacy behavior when a read-only capability probe fails', async () => {
const hostAuthority = vi.fn().mockResolvedValue('structured')
const legacy = vi.fn().mockResolvedValue('legacy')
mocks.supportsCapability.mockRejectedValue(new Error('status temporarily unavailable'))
await expect(
runRemoteAgentSessionLaunch({ environmentId: 'env-1', hostAuthority, legacy })
).resolves.toBe('legacy')
expect(legacy).toHaveBeenCalledOnce()
expect(hostAuthority).not.toHaveBeenCalled()
})
it('does not bypass an incompatible runtime protocol', async () => {
const compatibilityError = Object.assign(new Error('runtime incompatible'), {
code: 'runtime_compat_block'
})
const legacy = vi.fn().mockResolvedValue('legacy')
mocks.supportsCapability.mockRejectedValue(compatibilityError)
await expect(
runRemoteAgentSessionLaunch({
environmentId: 'env-1',
hostAuthority,
requiredHostAuthorityCapabilities: ['agent-session.omp-resume-path.v1'],
hostAuthority: vi.fn(),
legacy
})
).resolves.toBe('legacy')
expect(hostAuthority).not.toHaveBeenCalled()
expect(legacy).toHaveBeenCalledWith({ skipCompatibilityCheck: true, authority })
})
it('fails closed without attribution-removal capability evidence', async () => {
mocks.probe.mockResolvedValue({
supported: false,
authority: { ...authority, capabilities: ['agent-session.host-authority.v1'] }
})
const legacy = vi.fn().mockResolvedValue('legacy')
await expect(
runRemoteAgentSessionLaunch({ environmentId: 'env-1', hostAuthority: vi.fn(), legacy })
).rejects.toThrow('Update the host and try again')
expect(legacy).not.toHaveBeenCalled()
})
it('fails closed when the live capability probe fails', async () => {
mocks.probe.mockRejectedValue(
Object.assign(new Error('status temporarily unavailable'), { code: 'runtime_timeout' })
)
const legacy = vi.fn().mockResolvedValue('legacy')
const result = expect(
runRemoteAgentSessionLaunch({ environmentId: 'env-1', hostAuthority: vi.fn(), legacy })
).rejects
await result.toThrow('Update the host and try again')
await result.toMatchObject({ code: 'runtime_timeout' })
expect(legacy).not.toHaveBeenCalled()
})
it('preserves an incompatible runtime protocol error', async () => {
const compatibilityError = Object.assign(new Error('runtime incompatible'), {
code: 'runtime_compat_block'
})
mocks.probe.mockRejectedValue(compatibilityError)
const legacy = vi.fn().mockResolvedValue('legacy')
await expect(
runRemoteAgentSessionLaunch({ environmentId: 'env-1', hostAuthority: vi.fn(), legacy })
).rejects.toBe(compatibilityError)
expect(legacy).not.toHaveBeenCalled()
})
@@ -121,6 +88,7 @@ describe('remote agent-session launch routing', () => {
it('never downgrades after structured dispatch has started', async () => {
const structuredError = new Error('structured response was lost')
const legacy = vi.fn().mockResolvedValue('legacy')
mocks.supportsCapability.mockResolvedValue(true)
await expect(
runRemoteAgentSessionLaunch({
@@ -132,13 +100,14 @@ describe('remote agent-session launch routing', () => {
expect(legacy).not.toHaveBeenCalled()
})
it('uses fenced legacy for the pre-side-effect lower-owner response', async () => {
it('uses legacy only for the host pre-side-effect lower-owner response', async () => {
const legacy = vi.fn().mockResolvedValue('legacy')
mocks.supportsCapability.mockResolvedValue(true)
const legacyRequired = new RuntimeRpcCallError({
id: 'request-1',
ok: false,
error: { code: 'agent_session_legacy_required', message: 'legacy required' }
})
const legacy = vi.fn().mockResolvedValue('legacy')
await expect(
runRemoteAgentSessionLaunch({
@@ -147,16 +116,17 @@ describe('remote agent-session launch routing', () => {
legacy
})
).resolves.toBe('legacy')
expect(legacy).toHaveBeenCalledWith({ skipCompatibilityCheck: true, authority })
expect(legacy).toHaveBeenCalledOnce()
})
it('does not downgrade when a replacement host rejects the structured method', async () => {
it('uses legacy when a replaced old host does not recognize the structured method', async () => {
const legacy = vi.fn().mockResolvedValue('legacy')
mocks.supportsCapability.mockResolvedValue(true)
const methodNotFound = new RuntimeRpcCallError({
id: 'request-1',
ok: false,
error: { code: 'method_not_found', message: 'Unknown method' }
})
const legacy = vi.fn().mockResolvedValue('legacy')
await expect(
runRemoteAgentSessionLaunch({
@@ -164,16 +134,16 @@ describe('remote agent-session launch routing', () => {
hostAuthority: vi.fn().mockRejectedValue(methodNotFound),
legacy
})
).rejects.toBe(methodNotFound)
expect(legacy).not.toHaveBeenCalled()
).resolves.toBe('legacy')
expect(legacy).toHaveBeenCalledOnce()
})
it('probes and fences legacy when no structured form exists', async () => {
it('uses legacy directly when no structured form exists', async () => {
const legacy = vi.fn().mockResolvedValue('legacy')
await expect(runRemoteAgentSessionLaunch({ environmentId: 'env-1', legacy })).resolves.toBe(
'legacy'
)
expect(legacy).toHaveBeenCalledWith({ skipCompatibilityCheck: true, authority })
expect(mocks.supportsCapability).not.toHaveBeenCalled()
})
})
@@ -1,61 +1,46 @@
import { AGENT_SESSION_HOST_AUTHORITY_CAPABILITY } from '../../../shared/agent-session-host-authority'
import {
TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY,
type RuntimeCapability
} from '../../../shared/protocol-version'
import { TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE } from '../../../shared/legacy-terminal-attribution-env'
import {
probeLiveRuntimeEnvironmentCapabilities,
RuntimeRpcCallError,
type LiveRuntimeEnvironmentAuthority
} from './runtime-rpc-client'
import type { RuntimeCapability } from '../../../shared/protocol-version'
import { RuntimeRpcCallError, runtimeEnvironmentSupportsCapability } from './runtime-rpc-client'
import { isRuntimeCompatBlockError } from './runtime-protocol-compat'
export async function runRemoteAgentSessionLaunch<TResult>(args: {
environmentId: string
expectedEnvironmentPairingRevision?: number
hostAuthority?: (authority: LiveRuntimeEnvironmentAuthority) => Promise<TResult>
requiredHostAuthorityCapabilities?: readonly RuntimeCapability[]
legacy: (options: {
skipCompatibilityCheck: boolean
authority: LiveRuntimeEnvironmentAuthority
}) => Promise<TResult>
hostAuthority?: () => Promise<TResult>
hostAuthorityCapability?: RuntimeCapability
legacy: (options: { skipCompatibilityCheck: boolean }) => Promise<TResult>
}): Promise<TResult> {
const requiredCapabilities = [
TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY,
...(args.hostAuthority ? [AGENT_SESSION_HOST_AUTHORITY_CAPABILITY] : []),
...(args.requiredHostAuthorityCapabilities ?? [])
]
let probe: Awaited<ReturnType<typeof probeLiveRuntimeEnvironmentCapabilities>>
if (!args.hostAuthority) {
return await args.legacy({ skipCompatibilityCheck: false })
}
let supported: boolean
try {
probe = await probeLiveRuntimeEnvironmentCapabilities({
environmentId: args.environmentId,
requiredCapabilities,
timeoutMs: 15_000,
expectedEnvironmentPairingRevision: args.expectedEnvironmentPairingRevision
})
supported = await runtimeEnvironmentSupportsCapability(
args.environmentId,
args.hostAuthorityCapability ?? AGENT_SESSION_HOST_AUTHORITY_CAPABILITY
)
} catch (error) {
if (isRuntimeCompatBlockError(error)) {
throw error
}
const code = error && typeof error === 'object' ? Reflect.get(error, 'code') : undefined
// Why: unknown-outcome recovery still needs to classify a transient failed probe.
throw Object.assign(
new Error(TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE, { cause: error }),
typeof code === 'string' ? { code } : {}
)
// Why: a failed read-only probe has not launched anything, so preserving
// the legacy path cannot duplicate an agent and keeps transient upgrades neutral.
return await args.legacy({ skipCompatibilityCheck: true })
}
if (!probe.authority.capabilities.includes(TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY)) {
throw new Error(TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE)
}
if (!args.hostAuthority || !probe.supported) {
return await args.legacy({ skipCompatibilityCheck: true, authority: probe.authority })
// Why: choose before invoking either path; an ambiguous structured outcome
// must never trigger a legacy retry that could spawn a duplicate.
if (!supported) {
return await args.legacy({ skipCompatibilityCheck: true })
}
try {
return await args.hostAuthority(probe.authority)
return await args.hostAuthority()
} catch (error) {
if (error instanceof RuntimeRpcCallError && error.code === 'agent_session_legacy_required') {
return await args.legacy({ skipCompatibilityCheck: true, authority: probe.authority })
if (
error instanceof RuntimeRpcCallError &&
(error.code === 'agent_session_legacy_required' || error.code === 'method_not_found')
) {
// Why: both responses prove no structured side effect began: the new host rejected an old
// lower owner before dispatch, or an old host never recognized the method.
return await args.legacy({ skipCompatibilityCheck: true })
}
throw error
}
@@ -1,46 +0,0 @@
import type { RuntimeRpcResponse } from '../../../shared/runtime-rpc-envelope'
import type { RuntimeStatus } from '../../../shared/runtime-types'
import type { RuntimeCapability } from '../../../shared/protocol-version'
import { callRuntimeEnvironmentWithRevision } from './runtime-rpc-environment-call'
import { captureRuntimeEnvironmentRequestRevision } from './runtime-environment-revision'
import { assertRuntimeStatusCompatible } from './runtime-protocol-compat'
import { unwrapRuntimeRpcResult } from './runtime-rpc-result'
export type LiveRuntimeEnvironmentAuthority = Readonly<{
runtimeId: string
expectedEnvironmentPairingRevision: number | undefined
capabilities: readonly RuntimeCapability[]
}>
export async function probeLiveRuntimeEnvironmentCapabilities(args: {
environmentId: string
requiredCapabilities: readonly RuntimeCapability[]
timeoutMs?: number
expectedEnvironmentPairingRevision?: number
}): Promise<{ supported: boolean; authority: LiveRuntimeEnvironmentAuthority }> {
const environmentId = args.environmentId.trim()
const expectedEnvironmentPairingRevision = captureRuntimeEnvironmentRequestRevision(
environmentId,
args.expectedEnvironmentPairingRevision
)
const response = await callRuntimeEnvironmentWithRevision({
environmentId,
method: 'status.get',
params: undefined,
timeoutMs: args.timeoutMs,
expectedEnvironmentPairingRevision
})
const status = unwrapRuntimeRpcResult<RuntimeStatus>(
response as RuntimeRpcResponse<RuntimeStatus>
)
assertRuntimeStatusCompatible(status)
const capabilities = status.capabilities ?? []
return {
supported: args.requiredCapabilities.every((capability) => capabilities.includes(capability)),
authority: Object.freeze({
runtimeId: status.runtimeId,
expectedEnvironmentPairingRevision,
capabilities: Object.freeze([...capabilities])
})
}
}
@@ -3,11 +3,7 @@ import {
MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
RUNTIME_PROTOCOL_VERSION
} from '../../../shared/protocol-version'
import {
callRuntimeRpc,
clearRuntimeCompatibilityCacheForTests,
probeLiveRuntimeEnvironmentCapabilities
} from './runtime-rpc-client'
import { callRuntimeRpc, clearRuntimeCompatibilityCacheForTests } from './runtime-rpc-client'
import { replaceRuntimeEnvironmentRevisions } from './runtime-environment-revision'
const runtimeEnvironmentCall = vi.fn()
@@ -66,57 +62,3 @@ it('captures the pairing revision before awaiting the compatibility probe', asyn
expectedEnvironmentPairingRevision: 10
})
})
it('live capability probes ignore a warm cache and pin the captured pairing revision', async () => {
replaceRuntimeEnvironmentRevisions([{ id: 'env-live', createdAt: 1, pairingRevision: 20 }])
runtimeEnvironmentCall
.mockResolvedValueOnce({
id: 'warm-status',
ok: true,
result: {
runtimeId: 'runtime-a',
graphStatus: 'ready',
runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION,
minCompatibleRuntimeClientVersion: MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
capabilities: ['terminal.attribution-removed.v1']
}
})
.mockResolvedValueOnce({ id: 'repo', ok: true, result: { repos: [] } })
.mockImplementationOnce(async () => {
replaceRuntimeEnvironmentRevisions([{ id: 'env-live', createdAt: 1, pairingRevision: 21 }])
return {
id: 'live-status',
ok: true,
result: {
runtimeId: 'runtime-b',
graphStatus: 'ready',
runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION,
minCompatibleRuntimeClientVersion: MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
capabilities: []
}
}
})
await callRuntimeRpc({ kind: 'environment', environmentId: 'env-live' }, 'repo.list')
await expect(
probeLiveRuntimeEnvironmentCapabilities({
environmentId: 'env-live',
requiredCapabilities: ['terminal.attribution-removed.v1']
})
).resolves.toEqual({
supported: false,
authority: {
runtimeId: 'runtime-b',
expectedEnvironmentPairingRevision: 20,
capabilities: []
}
})
expect(runtimeEnvironmentCall).toHaveBeenLastCalledWith({
selector: 'env-live',
method: 'status.get',
params: undefined,
timeoutMs: undefined,
expectedEnvironmentPairingRevision: 20
})
})
@@ -19,8 +19,6 @@ export {
RuntimeRpcCallError,
unwrapRuntimeRpcResult
} from './runtime-rpc-result'
export { probeLiveRuntimeEnvironmentCapabilities } from './runtime-environment-authority'
export type { LiveRuntimeEnvironmentAuthority } from './runtime-environment-authority'
const RUNTIME_COMPATIBILITY_CACHE_MAX = 32
const RECENT_RUNTIME_COMPATIBILITY_FAILURE_TTL_MS = 60_000
@@ -56,7 +54,6 @@ export async function callRuntimeRpc<TResult>(
skipCompatibilityCheck?: boolean
signal?: AbortSignal
expectedEnvironmentPairingRevision?: number
expectedRuntimeId?: string
} = {}
): Promise<TResult> {
const expectedEnvironmentPairingRevision =
@@ -91,8 +88,7 @@ export async function callRuntimeRpc<TResult>(
params: nextParams,
timeoutMs: options.timeoutMs,
signal: options.signal,
expectedEnvironmentPairingRevision,
expectedRuntimeId: options.expectedRuntimeId
expectedEnvironmentPairingRevision
})
return unwrapRuntimeRpcResult<TResult>(response as RuntimeRpcResponse<TResult>)
}
@@ -1,28 +0,0 @@
import { expect, it, vi } from 'vitest'
import { callRuntimeEnvironmentWithRevision } from './runtime-rpc-environment-call'
it('preserves abort semantics for runtime-fenced requests', async () => {
const controller = new AbortController()
const unsubscribe = vi.fn()
const call = vi.fn()
const subscribe = vi.fn().mockResolvedValue({ unsubscribe, sendBinary: vi.fn() })
vi.stubGlobal('window', { api: { runtimeEnvironments: { call, subscribe } } })
const request = callRuntimeEnvironmentWithRevision({
environmentId: 'env-1',
method: 'status.get',
params: undefined,
signal: controller.signal,
expectedRuntimeId: 'runtime-1'
})
await vi.waitFor(() => expect(subscribe).toHaveBeenCalled())
expect(subscribe).toHaveBeenCalledWith(
expect.objectContaining({ expectedRuntimeId: 'runtime-1' }),
expect.any(Object)
)
controller.abort()
await expect(request).rejects.toMatchObject({ name: 'AbortError' })
await vi.waitFor(() => expect(unsubscribe).toHaveBeenCalledOnce())
expect(call).not.toHaveBeenCalled()
})
@@ -7,7 +7,6 @@ export async function callRuntimeEnvironmentWithRevision(args: {
timeoutMs?: number
signal?: AbortSignal
expectedEnvironmentPairingRevision?: number
expectedRuntimeId?: string
}): Promise<unknown> {
if (args.signal) {
return callAbortableRuntimeEnvironment(
@@ -16,8 +15,7 @@ export async function callRuntimeEnvironmentWithRevision(args: {
args.params,
args.timeoutMs,
args.signal,
args.expectedEnvironmentPairingRevision,
args.expectedRuntimeId
args.expectedEnvironmentPairingRevision
)
}
return window.api.runtimeEnvironments.call({
@@ -25,7 +23,6 @@ export async function callRuntimeEnvironmentWithRevision(args: {
method: args.method,
params: args.params,
timeoutMs: args.timeoutMs,
expectedEnvironmentPairingRevision: args.expectedEnvironmentPairingRevision,
expectedRuntimeId: args.expectedRuntimeId
expectedEnvironmentPairingRevision: args.expectedEnvironmentPairingRevision
})
}
@@ -56,12 +56,9 @@ const mocks = vi.hoisted(() => ({
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
getRuntimeEnvironmentIdForWorktree: vi.fn(),
hasMaterializedWebRuntimeBrowserPage: vi.fn(),
toastError: vi.fn()
hasMaterializedWebRuntimeBrowserPage: vi.fn()
}))
vi.mock('sonner', () => ({ toast: { error: mocks.toastError } }))
vi.mock('../store', () => ({
useAppStore: {
getState: mocks.getState,
@@ -115,21 +112,6 @@ function makeSnapshot(): RuntimeMobileSessionTabsResult {
}
}
function makeAttributionSafeStatus(capabilities: string[] = ['terminal.attribution-removed.v1']) {
return {
id: 'status',
ok: true,
result: {
runtimeId: 'runtime-1',
graphStatus: 'ready',
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
appVersion: '1.4.181',
capabilities
}
}
}
describe('refreshWebRuntimeSessionTabsSnapshot', () => {
afterEach(() => {
resetWebAgentSessionHandoffsForTests()
@@ -1448,7 +1430,6 @@ describe('createWebRuntimeSessionTerminal', () => {
})
const runtimeCall = vi
.fn()
.mockResolvedValueOnce(makeAttributionSafeStatus())
.mockResolvedValueOnce({
id: 'create',
ok: true,
@@ -1469,43 +1450,6 @@ describe('createWebRuntimeSessionTerminal', () => {
expect(selectedHosts).toEqual([RUNTIME_EXECUTION_HOST_ID])
})
it('refuses session-tab creation when an older host drops terminal environment fields', async () => {
const runtimeCall = vi.fn().mockResolvedValue({
id: 'status',
ok: true,
result: { capabilities: ['mobile.tasks.v1'] }
})
vi.stubGlobal('window', {
api: { runtimeEnvironments: { call: runtimeCall } }
})
await expect(createWebRuntimeSessionTerminal({ worktreeId: WORKTREE_ID })).resolves.toEqual({
status: 'failed',
message:
'Creating terminals requires a newer workspace host that can verify safe terminal environment forwarding. Update the host and try again.'
})
expect(runtimeCall.mock.calls.map(([request]) => request.method)).toEqual(['status.get'])
})
it('returns a failed outcome when attribution cleanup exceeds the deletion limit', async () => {
const runtimeCall = vi.fn()
vi.stubGlobal('window', {
api: { runtimeEnvironments: { call: runtimeCall } }
})
await expect(
createWebRuntimeSessionTerminal({
worktreeId: WORKTREE_ID,
envToDelete: Array.from({ length: 32 }, (_, index) => `KEY_${index}`)
})
).resolves.toEqual({
status: 'failed',
message: 'Terminal environment deletion limit leaves no room for attribution cleanup'
})
expect(runtimeCall).not.toHaveBeenCalled()
})
it.each([
{ sessionKind: 'fresh' as const, activate: true },
{ sessionKind: 'fresh' as const, activate: false },
@@ -1525,7 +1469,7 @@ describe('createWebRuntimeSessionTerminal', () => {
graphStatus: 'ready',
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: ['agent-session.host-authority.v1', 'terminal.attribution-removed.v1']
capabilities: ['agent-session.host-authority.v1']
}
}
}
@@ -1614,7 +1558,7 @@ describe('createWebRuntimeSessionTerminal', () => {
graphStatus: 'ready',
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: ['agent-session.host-authority.v1', 'terminal.attribution-removed.v1']
capabilities: ['agent-session.host-authority.v1']
}
})
.mockResolvedValueOnce({
@@ -1678,7 +1622,6 @@ describe('createWebRuntimeSessionTerminal', () => {
expect(runtimeCall).toHaveBeenNthCalledWith(2, {
selector: ENVIRONMENT_ID,
expectedEnvironmentPairingRevision: undefined,
expectedRuntimeId: 'runtime-1',
method: 'terminal.createAgentSession',
params: {
clientOperationId: expect.stringMatching(/^\d{13}-[0-9a-f]{32}$/),
@@ -1723,7 +1666,6 @@ describe('createWebRuntimeSessionTerminal', () => {
it('keeps exact legacy ordering when structured creation cannot express afterTabId', async () => {
const runtimeCall = vi
.fn()
.mockResolvedValueOnce(makeAttributionSafeStatus())
.mockResolvedValueOnce({
id: 'legacy-create',
ok: true,
@@ -1749,10 +1691,8 @@ describe('createWebRuntimeSessionTerminal', () => {
})
).resolves.toEqual({ status: 'created' })
expect(runtimeCall).toHaveBeenNthCalledWith(2, {
expect(runtimeCall).toHaveBeenNthCalledWith(1, {
selector: ENVIRONMENT_ID,
expectedEnvironmentPairingRevision: undefined,
expectedRuntimeId: 'runtime-1',
method: 'session.tabs.createTerminal',
params: {
worktree: `id:${WORKTREE_ID}`,
@@ -1760,8 +1700,6 @@ describe('createWebRuntimeSessionTerminal', () => {
targetGroupId: 'group-left',
command: undefined,
cwd: undefined,
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
startupCommandDelivery: undefined,
agent: 'codex',
activate: false,
@@ -1771,7 +1709,6 @@ describe('createWebRuntimeSessionTerminal', () => {
timeoutMs: 15_000
})
expect(runtimeCall.mock.calls.map(([request]) => request.method)).toEqual([
'status.get',
'session.tabs.createTerminal',
'session.tabs.list'
])
@@ -1788,7 +1725,6 @@ describe('createWebRuntimeSessionTerminal', () => {
})
const runtimeCall = vi
.fn()
.mockResolvedValueOnce(makeAttributionSafeStatus())
.mockResolvedValueOnce({
id: 'create-terminal',
ok: true,
@@ -1845,7 +1781,7 @@ describe('createWebRuntimeSessionTerminal', () => {
graphStatus: 'ready',
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: ['agent-session.host-authority.v1', 'terminal.attribution-removed.v1']
capabilities: ['agent-session.host-authority.v1']
}
}
}
@@ -1906,7 +1842,7 @@ describe('createWebRuntimeSessionTerminal', () => {
graphStatus: 'ready',
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: ['agent-session.host-authority.v1', 'terminal.attribution-removed.v1']
capabilities: ['agent-session.host-authority.v1']
}
}
}
@@ -1959,7 +1895,7 @@ describe('createWebRuntimeSessionTerminal', () => {
graphStatus: 'ready',
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: ['terminal.attribution-removed.v1']
capabilities: []
}
}
}
@@ -1990,8 +1926,6 @@ describe('createWebRuntimeSessionTerminal', () => {
expect(runtimeCall).toHaveBeenNthCalledWith(2, {
selector: ENVIRONMENT_ID,
expectedEnvironmentPairingRevision: undefined,
expectedRuntimeId: 'old-runtime',
method: 'session.tabs.createTerminal',
params: {
worktree: `id:${WORKTREE_ID}`,
@@ -1999,8 +1933,6 @@ describe('createWebRuntimeSessionTerminal', () => {
targetGroupId: 'group-left',
command: undefined,
cwd: undefined,
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
startupCommandDelivery: undefined,
launchAgent: 'codex',
activate: false,
@@ -2027,7 +1959,7 @@ describe('createWebRuntimeSessionTerminal', () => {
graphStatus: 'ready',
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: ['terminal.attribution-removed.v1']
capabilities: []
}
}
}
@@ -2062,8 +1994,6 @@ describe('createWebRuntimeSessionTerminal', () => {
expect(runtimeCall).toHaveBeenNthCalledWith(2, {
selector: ENVIRONMENT_ID,
expectedEnvironmentPairingRevision: undefined,
expectedRuntimeId: 'old-runtime',
method: 'session.tabs.createTerminal',
params: {
worktree: `id:${WORKTREE_ID}`,
@@ -2071,8 +2001,7 @@ describe('createWebRuntimeSessionTerminal', () => {
targetGroupId: undefined,
command: "codex resume 'session-1'",
cwd: undefined,
env: { CODEX_PROFILE: 'captured', ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
env: { CODEX_PROFILE: 'captured' },
startupCommandDelivery: undefined,
launchConfig: {
agentCommand: 'codex',
@@ -2101,7 +2030,7 @@ describe('createWebRuntimeSessionTerminal', () => {
graphStatus: 'ready',
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: ['agent-session.host-authority.v1', 'terminal.attribution-removed.v1']
capabilities: ['agent-session.host-authority.v1']
}
}
}
@@ -2142,14 +2071,9 @@ describe('createWebRuntimeSessionTerminal', () => {
expect(methods).toEqual(['status.get', 'session.tabs.createTerminal', 'session.tabs.list'])
expect(runtimeCall.mock.calls[1]?.[0]).toMatchObject({
expectedRuntimeId: 'new-runtime',
params: {
command: "omp --resume '/custom/omp/project/session.jsonl'",
env: {
PI_CODING_AGENT_DIR: '/custom/omp',
ORCA_ATTRIBUTION_BYPASS: '1'
},
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
env: { PI_CODING_AGENT_DIR: '/custom/omp' },
launchAgent: 'omp'
}
})
@@ -2166,7 +2090,7 @@ describe('createWebRuntimeSessionTerminal', () => {
graphStatus: 'ready',
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: ['agent-session.host-authority.v1', 'terminal.attribution-removed.v1']
capabilities: ['agent-session.host-authority.v1']
}
}
}
@@ -2228,7 +2152,7 @@ describe('createWebRuntimeSessionTerminal', () => {
graphStatus: 'ready',
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: ['agent-session.host-authority.v1', 'terminal.attribution-removed.v1']
capabilities: ['agent-session.host-authority.v1']
}
}
}
@@ -2858,25 +2782,17 @@ describe('splitWebRuntimeTerminal', () => {
})
it('passes telemetry source to the host split while allowing the mirrored split event to be suppressed', async () => {
const runtimeCall = vi.fn(async (request: { method: string }) =>
request.method === 'status.get'
? {
id: 'status',
ok: true,
result: { capabilities: ['terminal.attribution-removed.v1'] }
}
: {
id: 'split',
ok: true,
result: {
split: {
handle: 'terminal-2',
tabId: 'tab-1',
paneRuntimeId: -1
}
}
}
)
const runtimeCall = vi.fn().mockResolvedValue({
id: 'split',
ok: true,
result: {
split: {
handle: 'terminal-2',
tabId: 'tab-1',
paneRuntimeId: -1
}
}
})
vi.stubGlobal('window', {
api: {
runtimeEnvironments: {
@@ -2895,22 +2811,13 @@ describe('splitWebRuntimeTerminal', () => {
consumePendingWebRuntimeSplitMirrorTelemetry('remote:web-env-1@@terminal-1', 'horizontal')
).toBe(true)
await vi.waitFor(() => expect(runtimeCall).toHaveBeenCalledTimes(2))
expect(runtimeCall).toHaveBeenNthCalledWith(1, {
await vi.waitFor(() => expect(runtimeCall).toHaveBeenCalledTimes(1))
expect(runtimeCall).toHaveBeenCalledWith({
selector: 'web-env-1',
expectedEnvironmentPairingRevision: undefined,
method: 'status.get',
timeoutMs: 15_000
})
expect(runtimeCall).toHaveBeenNthCalledWith(2, {
selector: 'web-env-1',
expectedEnvironmentPairingRevision: undefined,
method: 'terminal.split',
params: {
terminal: 'terminal-1',
direction: 'horizontal',
env: { ORCA_ATTRIBUTION_BYPASS: '1' },
envToDelete: ['ORCA_ENABLE_GIT_ATTRIBUTION'],
telemetrySource: 'keyboard'
},
timeoutMs: 15_000
@@ -2919,19 +2826,11 @@ describe('splitWebRuntimeTerminal', () => {
it('does not track rejected host split RPCs', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const runtimeCall = vi.fn(async (request: { method: string }) =>
request.method === 'status.get'
? {
id: 'status',
ok: true,
result: { capabilities: ['terminal.attribution-removed.v1'] }
}
: {
id: 'split',
ok: false,
error: { code: 'terminal_exited', message: 'Terminal exited' }
}
)
const runtimeCall = vi.fn().mockResolvedValue({
id: 'split',
ok: false,
error: { code: 'terminal_exited', message: 'Terminal exited' }
})
vi.stubGlobal('window', {
api: {
runtimeEnvironments: {
@@ -2944,53 +2843,23 @@ describe('splitWebRuntimeTerminal', () => {
splitWebRuntimeTerminal('remote:web-env-1@@terminal-1', 'vertical', 'context_menu')
).toBe(true)
await vi.waitFor(() => expect(runtimeCall).toHaveBeenCalledTimes(2))
await vi.waitFor(() => expect(runtimeCall).toHaveBeenCalledTimes(1))
await vi.waitFor(() => expect(warnSpy).toHaveBeenCalledTimes(1))
expect(mocks.toastError).toHaveBeenCalledWith('Terminal exited')
expect(mocks.trackTerminalPaneSplit).not.toHaveBeenCalled()
})
it('refuses legacy hosts because renderer-owned splits discard environment fields', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const runtimeCall = vi.fn().mockResolvedValue({
id: 'status',
ok: true,
result: { appVersion: '1.4.181', capabilities: ['mobile.tasks.v1'] }
})
vi.stubGlobal('window', {
api: { runtimeEnvironments: { call: runtimeCall } }
})
expect(splitWebRuntimeTerminal('remote:web-env-1@@terminal-1', 'vertical', 'keyboard')).toBe(
true
)
await vi.waitFor(() => expect(warnSpy).toHaveBeenCalledTimes(1))
expect(runtimeCall).toHaveBeenCalledTimes(1)
expect(runtimeCall).not.toHaveBeenCalledWith(
expect.objectContaining({ method: 'terminal.split' })
)
expect(mocks.toastError).toHaveBeenCalledWith(
expect.stringContaining('Update the host and try again')
)
})
it('ignores local panes but delegates remote runtime panes from desktop or web clients', async () => {
const runtimeCall = vi.fn(async (request: { method: string }) =>
request.method === 'status.get'
? { id: 'status', ok: true, result: { capabilities: ['terminal.attribution-removed.v1'] } }
: {
id: 'split',
ok: true,
result: {
split: {
handle: 'terminal-2',
tabId: 'tab-1',
ptyId: 'pty-2'
}
}
}
)
const runtimeCall = vi.fn().mockResolvedValue({
id: 'split',
ok: true,
result: {
split: {
handle: 'terminal-2',
tabId: 'tab-1',
ptyId: 'pty-2'
}
}
})
vi.stubGlobal('window', {
api: {
runtimeEnvironments: {
@@ -3005,7 +2874,7 @@ describe('splitWebRuntimeTerminal', () => {
true
)
await vi.waitFor(() => expect(runtimeCall).toHaveBeenCalledTimes(2))
await vi.waitFor(() => expect(runtimeCall).toHaveBeenCalledTimes(1))
})
})
+28 -70
View File
@@ -1,4 +1,5 @@
/* eslint-disable max-lines */
import { toast } from 'sonner'
import type { RuntimeRpcResponse } from '../../../shared/runtime-rpc-envelope'
import type {
BrowserTabCreateResult,
@@ -8,7 +9,6 @@ import type {
RuntimeMobileSessionTabMoveResult,
RuntimeMobileSessionTabsResult,
RuntimeSessionTabCloseReason,
RuntimeStatus,
RuntimeTerminalCreate,
RuntimeTerminalClose,
RuntimeTerminalSplit
@@ -19,10 +19,7 @@ import type {
SleepingAgentLaunchConfig,
AgentProviderSessionMetadata
} from '../../../shared/agent-session-resume'
import {
AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY,
TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY
} from '../../../shared/protocol-version'
import { AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY } from '../../../shared/protocol-version'
import type {
AgentLaunchPreferences,
AgentPromptDelivery,
@@ -85,15 +82,6 @@ import {
throwIfE2eWebRuntimeBrowserCapabilityUnavailable,
throwIfE2eWebRuntimeBrowserReconciliationFails
} from './web-runtime-browser-creation-e2e-fault'
import {
addLegacyTerminalAttributionDisableRequest,
hostSupportsSessionTabTerminalCreateAttributionDisable,
hostSupportsTerminalSplitAttributionDisable,
SESSION_TAB_TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE,
TERMINAL_SPLIT_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE,
withLegacyTerminalAttributionDisabledEnv
} from '../../../shared/legacy-terminal-attribution-env'
import { toast } from 'sonner'
export {
HOST_TERMINAL_SURFACE_SEPARATOR,
@@ -143,7 +131,6 @@ function captureRuntimeEnvironmentCall(
method: string
params?: unknown
timeoutMs?: number
expectedRuntimeId?: string
}) => Promise<RuntimeRpcResponse<unknown>> {
return (args) =>
window.api.runtimeEnvironments.call({
@@ -278,14 +265,6 @@ async function createWebRuntimeSessionTerminalResult(
}
const intentOwner = captureWebSessionIntentOwner(environmentId)
const callEnvironment = captureRuntimeEnvironmentCall(environmentId, intentOwner.pairingRevision)
const assertSessionTabCreateAttributionDisableSupported = async (): Promise<RuntimeStatus> => {
const response = await callEnvironment({ method: 'status.get', timeoutMs: 15_000 })
const status = unwrapRuntimeRpcResult(response as RuntimeRpcResponse<RuntimeStatus>)
if (!hostSupportsSessionTabTerminalCreateAttributionDisable(status)) {
throw new Error(SESSION_TAB_TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE)
}
return status
}
if (args.selectWorktree !== false) {
selectWebRuntimeSessionWorktree(args.worktreeId, environmentId)
@@ -294,8 +273,6 @@ async function createWebRuntimeSessionTerminalResult(
let createdTabId: string | undefined
let createdLeafId: string | undefined
try {
const env = withLegacyTerminalAttributionDisabledEnv(args.env)
const envToDelete = addLegacyTerminalAttributionDisableRequest(args.envToDelete)
const agent = args.launchAgent ?? args.agent
const agentArgsOverride =
args.agentArgs !== undefined ? args.agentArgs : args.launchConfig?.agentArgs
@@ -307,7 +284,7 @@ async function createWebRuntimeSessionTerminalResult(
? undefined
: args.agentSessionKind === 'resume'
? args.providerSession
? async (authority: { runtimeId: string }) =>
? async () =>
unwrapRuntimeRpcResult(
(await callEnvironment({
method: 'terminal.ensureAgentSession',
@@ -325,12 +302,11 @@ async function createWebRuntimeSessionTerminalResult(
: {}),
presentation: 'background'
},
timeoutMs: 15_000,
expectedRuntimeId: authority.runtimeId
timeoutMs: 15_000
})) as RuntimeRpcResponse<RuntimeEnsureAgentSessionResult>
)
: undefined
: async (authority: { runtimeId: string }) =>
: async () =>
await createAgentSessionCreateOperation().run(async (clientOperationId) =>
unwrapRuntimeRpcResult(
(await callEnvironment({
@@ -353,8 +329,7 @@ async function createWebRuntimeSessionTerminalResult(
},
clientOperationId
),
timeoutMs: 15_000,
expectedRuntimeId: authority.runtimeId
timeoutMs: 15_000
})) as RuntimeRpcResponse<RuntimeCreateAgentSessionResult>
)
)
@@ -362,15 +337,11 @@ async function createWebRuntimeSessionTerminalResult(
terminal: CreatedAgentTerminalIdentity
}>({
environmentId,
expectedEnvironmentPairingRevision: intentOwner.pairingRevision,
...(hostAuthority ? { hostAuthority } : {}),
requiredHostAuthorityCapabilities: [
TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY,
...(args.agentSessionKind === 'resume' && agent === 'omp'
? [AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY]
: [])
],
legacy: async ({ authority }) => {
...(args.agentSessionKind === 'resume' && agent === 'omp'
? { hostAuthorityCapability: AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY }
: {}),
legacy: async () => {
const response = await callEnvironment({
method: 'session.tabs.createTerminal',
params: {
@@ -379,8 +350,8 @@ async function createWebRuntimeSessionTerminalResult(
targetGroupId: args.targetGroupId,
command: args.command,
cwd: args.cwd,
env,
...(envToDelete ? { envToDelete } : {}),
...(args.env ? { env: args.env } : {}),
...(args.envToDelete ? { envToDelete: args.envToDelete } : {}),
startupCommandDelivery: args.startupCommandDelivery,
...(args.launchConfig ? { launchConfig: args.launchConfig } : {}),
...(args.launchToken ? { launchToken: args.launchToken } : {}),
@@ -392,8 +363,7 @@ async function createWebRuntimeSessionTerminalResult(
select: args.activate !== false,
navigation: 'caller'
},
timeoutMs: 15_000,
expectedRuntimeId: authority.runtimeId
timeoutMs: 15_000
})
const legacyCreated = unwrapRuntimeRpcResult(
response as RuntimeRpcResponse<RuntimeMobileSessionCreateTerminalResult>
@@ -425,7 +395,6 @@ async function createWebRuntimeSessionTerminalResult(
})
}
} else {
const status = await assertSessionTabCreateAttributionDisableSupported()
const response = await callEnvironment({
method: 'session.tabs.createTerminal',
params: {
@@ -434,8 +403,8 @@ async function createWebRuntimeSessionTerminalResult(
targetGroupId: args.targetGroupId,
command: args.command,
cwd: args.cwd,
env,
...(envToDelete ? { envToDelete } : {}),
...(args.env ? { env: args.env } : {}),
...(args.envToDelete ? { envToDelete: args.envToDelete } : {}),
startupCommandDelivery: args.startupCommandDelivery,
...(args.launchConfig ? { launchConfig: args.launchConfig } : {}),
...(args.launchToken ? { launchToken: args.launchToken } : {}),
@@ -445,8 +414,7 @@ async function createWebRuntimeSessionTerminalResult(
select: args.activate !== false,
navigation: 'caller'
},
timeoutMs: 15_000,
expectedRuntimeId: status.runtimeId
timeoutMs: 15_000
})
const created = unwrapRuntimeRpcResult(
response as RuntimeRpcResponse<RuntimeMobileSessionCreateTerminalResult>
@@ -1218,28 +1186,16 @@ export function splitWebRuntimeTerminal(
direction,
pendingMirrorSuppressionId
)
const callEnvironment = captureRuntimeEnvironmentCall(environmentId)
void callEnvironment({
method: 'status.get',
timeoutMs: 15_000
})
.then((response) => {
const status = unwrapRuntimeRpcResult(response as RuntimeRpcResponse<RuntimeStatus>)
if (!hostSupportsTerminalSplitAttributionDisable(status)) {
throw new Error(TERMINAL_SPLIT_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE)
}
return callEnvironment({
method: 'terminal.split',
params: {
terminal: remote.handle,
direction,
env: withLegacyTerminalAttributionDisabledEnv(undefined),
envToDelete: addLegacyTerminalAttributionDisableRequest(undefined),
telemetrySource
},
timeoutMs: 15_000,
expectedRuntimeId: status.runtimeId
})
void window.api.runtimeEnvironments
.call({
selector: environmentId,
method: 'terminal.split',
params: {
terminal: remote.handle,
direction,
telemetrySource
},
timeoutMs: 15_000
})
.then((response) => {
unwrapRuntimeRpcResult(response as RuntimeRpcResponse<{ split: RuntimeTerminalSplit }>)
@@ -1247,6 +1203,8 @@ export function splitWebRuntimeTerminal(
.catch((error) => {
releasePendingMirrorSuppression()
const message = error instanceof Error ? error.message : String(error)
// Why: a split that fails only in the console leaves the user with a pane that silently
// never appears.
toast.error(message)
console.warn('[web-runtime-session] failed to split terminal:', message)
})
@@ -1,99 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
addLegacyTerminalAttributionDisableRequest,
hostSupportsSessionTabTerminalCreateAttributionDisable,
hostSupportsTerminalCreateAttributionDisable,
hostSupportsTerminalSplitAttributionDisable,
LEGACY_TERMINAL_ATTRIBUTION_BYPASS_ENV_KEY,
LEGACY_TERMINAL_ATTRIBUTION_ENABLE_ENV_KEY,
withLegacyTerminalAttributionDisabledEnv
} from './legacy-terminal-attribution-env'
describe('legacy terminal attribution environment', () => {
it('adds the inert old-host gate once', () => {
expect(addLegacyTerminalAttributionDisableRequest(['CODEX_HOME'])).toEqual([
'CODEX_HOME',
LEGACY_TERMINAL_ATTRIBUTION_ENABLE_ENV_KEY
])
expect(
addLegacyTerminalAttributionDisableRequest([LEGACY_TERMINAL_ATTRIBUTION_ENABLE_ENV_KEY])
).toEqual([LEGACY_TERMINAL_ATTRIBUTION_ENABLE_ENV_KEY])
})
it('fails instead of dropping a caller deletion at the wire limit', () => {
const full = Array.from({ length: 32 }, (_, index) => `KEY_${index}`)
expect(() => addLegacyTerminalAttributionDisableRequest(full)).toThrow(
'Terminal environment deletion limit'
)
})
it('adds the wrapper bypass without mutating caller env', () => {
const env = { CODEX_HOME: '/tmp/codex' }
expect(withLegacyTerminalAttributionDisabledEnv(env)).toEqual({
CODEX_HOME: '/tmp/codex',
[LEGACY_TERMINAL_ATTRIBUTION_BYPASS_ENV_KEY]: '1'
})
expect(env).toEqual({ CODEX_HOME: '/tmp/codex' })
})
it('accepts only split hosts that removed attribution for every pane owner', () => {
expect(hostSupportsTerminalSplitAttributionDisable({ appVersion: '1.4.18' })).toBe(false)
expect(hostSupportsTerminalSplitAttributionDisable({ appVersion: '1.4.19' })).toBe(false)
expect(hostSupportsTerminalSplitAttributionDisable({ appVersion: 'v1.4.181' })).toBe(false)
expect(hostSupportsTerminalSplitAttributionDisable({ capabilities: ['mobile.tasks.v1'] })).toBe(
false
)
expect(
hostSupportsTerminalSplitAttributionDisable({
capabilities: ['terminal.attribution-removed.v1']
})
).toBe(true)
})
it('accepts only capability-proven terminal creation hosts', () => {
expect(hostSupportsSessionTabTerminalCreateAttributionDisable({ appVersion: '1.4.89' })).toBe(
false
)
expect(hostSupportsSessionTabTerminalCreateAttributionDisable({ appVersion: '1.4.90' })).toBe(
false
)
expect(
hostSupportsSessionTabTerminalCreateAttributionDisable({ appVersion: 'not-semver' })
).toBe(false)
expect(
hostSupportsSessionTabTerminalCreateAttributionDisable({
capabilities: ['workspace-run-context.v1']
})
).toBe(false)
expect(
hostSupportsSessionTabTerminalCreateAttributionDisable({
capabilities: ['terminal.quick-commands.v1']
})
).toBe(false)
})
it('accepts capability-proven current hosts regardless of development version', () => {
const status = {
appVersion: '0.0.0-dev',
capabilities: ['terminal.attribution-removed.v1']
}
expect(hostSupportsTerminalSplitAttributionDisable(status)).toBe(true)
expect(hostSupportsTerminalCreateAttributionDisable(status)).toBe(true)
expect(hostSupportsSessionTabTerminalCreateAttributionDisable(status)).toBe(true)
})
it('fails closed for malformed host status fields', () => {
expect(
hostSupportsSessionTabTerminalCreateAttributionDisable({
appVersion: 1.49,
capabilities: 'terminal.attribution-removed.v1'
})
).toBe(false)
expect(
hostSupportsTerminalSplitAttributionDisable({
appVersion: {},
capabilities: ['terminal.attribution-removed.v1', 1]
})
).toBe(false)
})
})
@@ -1,68 +0,0 @@
import { TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY } from './protocol-version'
export const LEGACY_TERMINAL_ATTRIBUTION_ENABLE_ENV_KEY = 'ORCA_ENABLE_GIT_ATTRIBUTION'
export const LEGACY_TERMINAL_ATTRIBUTION_BYPASS_ENV_KEY = 'ORCA_ATTRIBUTION_BYPASS'
export const TERMINAL_SPLIT_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE =
'Terminal splitting requires an updated workspace host that can verify attribution removal for every pane owner. Update the host and try again.'
export const TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE =
'Creating terminals requires an updated workspace host that can verify attribution removal. Update the host and try again.'
export const MOBILE_TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE =
'Creating terminals from Orca Mobile requires a newer workspace host that can verify safe terminal environment forwarding. Update the host and try again.'
export const SESSION_TAB_TERMINAL_CREATE_ATTRIBUTION_UPDATE_REQUIRED_MESSAGE =
'Creating terminals requires a newer workspace host that can verify safe terminal environment forwarding. Update the host and try again.'
const MAX_TERMINAL_ENV_DELETION_KEYS = 32
type TerminalAttributionHostStatus = {
appVersion?: string
capabilities?: string[]
}
function readTerminalAttributionHostStatus(value: unknown): TerminalAttributionHostStatus {
if (!value || typeof value !== 'object') {
return {}
}
const appVersion = Reflect.get(value, 'appVersion')
const capabilities = Reflect.get(value, 'capabilities')
return {
...(typeof appVersion === 'string' ? { appVersion } : {}),
...(Array.isArray(capabilities) && capabilities.every((entry) => typeof entry === 'string')
? { capabilities }
: {})
}
}
export function hostSupportsTerminalSplitAttributionDisable(value: unknown): boolean {
const status = readTerminalAttributionHostStatus(value)
// Why: only removal-capable hosts cover both runtime- and renderer-owned split targets.
return status.capabilities?.includes(TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY) === true
}
export function hostSupportsTerminalCreateAttributionDisable(value: unknown): boolean {
const status = readTerminalAttributionHostStatus(value)
return status.capabilities?.includes(TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY) === true
}
export function hostSupportsSessionTabTerminalCreateAttributionDisable(value: unknown): boolean {
const status = readTerminalAttributionHostStatus(value)
return status.capabilities?.includes(TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY) === true
}
export function withLegacyTerminalAttributionDisabledEnv(
env: Record<string, string> | undefined
): Record<string, string> {
return { ...env, [LEGACY_TERMINAL_ATTRIBUTION_BYPASS_ENV_KEY]: '1' }
}
export function addLegacyTerminalAttributionDisableRequest(
envToDelete: readonly string[] | undefined
): string[] | undefined {
const deduplicated = [...new Set(envToDelete ?? [])]
const gateIndex = deduplicated.indexOf(LEGACY_TERMINAL_ATTRIBUTION_ENABLE_ENV_KEY)
if (gateIndex !== -1) {
deduplicated.splice(gateIndex, 1)
}
if (deduplicated.length >= MAX_TERMINAL_ENV_DELETION_KEYS) {
throw new Error('Terminal environment deletion limit leaves no room for attribution cleanup')
}
return [...deduplicated, LEGACY_TERMINAL_ATTRIBUTION_ENABLE_ENV_KEY]
}
-4
View File
@@ -84,9 +84,6 @@ export const AGENT_SESSION_HOST_AUTHORITY_RUNTIME_CAPABILITY =
'agent-session.host-authority.v1' as const
export const AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY =
'agent-session.omp-resume-path.v1' as const
// Why: old host-authority launches cannot accept env deletions and may still inject attribution.
export const TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY =
'terminal.attribution-removed.v1' as const
// Why: older runtimes strip mutation owner fields, so clients must fence writes before RPC.
export const FILE_MUTATION_OWNERSHIP_RUNTIME_CAPABILITY = 'files.mutation-ownership.v1' as const
export const FILE_MUTATION_OWNERSHIP_UPDATE_REQUIRED_MESSAGE =
@@ -124,7 +121,6 @@ export const RUNTIME_CAPABILITIES = [
REMOTE_SERVER_UPDATE_CAPABILITY,
AGENT_SESSION_HOST_AUTHORITY_RUNTIME_CAPABILITY,
AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY,
TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY,
FILE_MUTATION_OWNERSHIP_RUNTIME_CAPABILITY,
ACCOUNT_IMPORT_RUNTIME_CAPABILITY,
CODEX_RESET_CREDIT_RUNTIME_CAPABILITY
+2 -27
View File
@@ -35,25 +35,6 @@ afterEach(async () => {
})
describe('subscribeRemoteRuntimeRequest', () => {
it('serializes the expected runtime fence on subscription requests', async () => {
const server = await createSubscriptionServer()
const subscription = await subscribeRemoteRuntimeRequest(
server.pairing,
'terminal.subscribe',
{},
1000,
{ onResponse: vi.fn(), onError: vi.fn() },
undefined,
{ expectedRuntimeId: 'runtime-1' }
)
await expect(server.nextRequest).resolves.toMatchObject({
method: 'terminal.subscribe',
expectedRuntimeId: 'runtime-1'
})
subscription.close()
})
it('includes WebSocket close details when subscription admission is rejected', async () => {
const server = await createClosingServer(1013, 'Maximum connections reached')
@@ -354,7 +335,6 @@ async function createSubscriptionServer(
pairing: PairingOffer
nextBinary: Promise<Uint8Array>
nextAuth: Promise<unknown>
nextRequest: Promise<Record<string, unknown>>
}> {
const serverKeyPair = generateKeyPair()
let resolveBinary: (bytes: Uint8Array) => void = () => {}
@@ -365,10 +345,6 @@ async function createSubscriptionServer(
const nextAuth = new Promise<unknown>((resolve) => {
resolveAuth = resolve
})
let resolveRequest: (request: Record<string, unknown>) => void = () => {}
const nextRequest = new Promise<Record<string, unknown>>((resolve) => {
resolveRequest = resolve
})
const wss = new WebSocketServer({ port: 0, autoPong: options.disableAutoPong !== true })
servers.push(wss)
@@ -410,8 +386,7 @@ async function createSubscriptionServer(
return
}
const request = JSON.parse(plaintext) as { id: string } & Record<string, unknown>
resolveRequest(request)
const request = JSON.parse(plaintext) as { id: string }
sendEncrypted(ws, sharedKey, {
id: request.id,
ok: true,
@@ -444,7 +419,7 @@ async function createSubscriptionServer(
if (!pairing) {
throw new Error('Failed to create test pairing')
}
return { pairing, nextBinary, nextAuth, nextRequest }
return { pairing, nextBinary, nextAuth }
}
function sendEncrypted(ws: WebSocket, sharedKey: Uint8Array, message: unknown): void {
+2 -4
View File
@@ -497,16 +497,14 @@ export async function subscribeRemoteRuntimeRequest<TResult>(
params: unknown,
timeoutMs: number,
callbacks: RemoteRuntimeSubscriptionCallbacks<TResult>,
livenessOptions?: RemoteRuntimeSocketLivenessOptions,
envelope?: RuntimeOrchestrationEnvelope
livenessOptions?: RemoteRuntimeSocketLivenessOptions
): Promise<RemoteRuntimeSubscription> {
const requestId = randomUUID()
const serializedRequest = serializeRemoteRuntimeRpcRequest({
requestId,
deviceToken: pairing.deviceToken,
method,
params,
envelope
params
})
const serializedAuth = serializeRemoteRuntimePayload({
type: 'e2ee_auth',
@@ -71,7 +71,6 @@ export function serializeRemoteRuntimeRpcRequest(args: {
deviceToken: args.deviceToken,
method: args.method,
params: args.params,
expectedRuntimeId: args.envelope?.expectedRuntimeId,
orchestrationCapability: args.envelope?.orchestrationCapability,
orchestrationContractVersion: args.envelope?.orchestrationContractVersion,
orchestrationRequestId: args.envelope?.orchestrationRequestId,
@@ -12,7 +12,6 @@ export function admitSharedControlSubscription(args: {
deviceToken: string
method: string
params: unknown
expectedRuntimeId?: string
}): number {
if (args.subscriptions.size >= REMOTE_RUNTIME_MAX_SUBSCRIPTIONS) {
throw new RemoteRuntimeClientError(
@@ -34,18 +33,12 @@ export function admitSharedControlSubscription(args: {
return retainedParamsBytes
}
function serializeRequest(args: {
deviceToken: string
method: string
params: unknown
expectedRuntimeId?: string
}): void {
function serializeRequest(args: { deviceToken: string; method: string; params: unknown }): void {
serializeRemoteRuntimeRpcRequest({
requestId: '00000000-0000-4000-8000-000000000000',
deviceToken: args.deviceToken,
method: args.method,
params: args.params,
envelope: { expectedRuntimeId: args.expectedRuntimeId }
params: args.params
})
}
@@ -37,7 +37,7 @@ export class RemoteRuntimeSharedControlConnection {
private sharedKey: Uint8Array | null = null
private socketCleanup: (() => void) | null = null
private readonly reconnect = new SharedControlReconnectScheduler()
private readonly stableReset: SharedControlReadyStableResetTimer
private readonly readyStableReset: SharedControlReadyStableResetTimer
private intentionallyClosed = false
private lastConnectedAt: number | null = null
private lastClose: { code: number; reason: string } | null = null
@@ -51,13 +51,15 @@ export class RemoteRuntimeSharedControlConnection {
constructor(
private readonly pairing: PairingOffer,
private readonly opts: {
private readonly options: {
environmentId?: string
reconnectStableResetMs?: number
liveness?: RemoteRuntimeSocketLivenessOptions
} = {}
) {
this.stableReset = new SharedControlReadyStableResetTimer(opts.reconnectStableResetMs ?? 30_000)
this.readyStableReset = new SharedControlReadyStableResetTimer(
options.reconnectStableResetMs ?? 30_000
)
}
request<TResult>(
@@ -83,15 +85,13 @@ export class RemoteRuntimeSharedControlConnection {
method: string,
params: unknown,
timeoutMs: number,
callbacks: SharedControlSubscriptionCallbacks<TResult>,
expectedRuntimeId?: string
callbacks: SharedControlSubscriptionCallbacks<TResult>
): Promise<RemoteRuntimeSharedSubscription> {
return startSharedControlSubscription({
subscriptions: this.subscriptions,
deviceToken: this.pairing.deviceToken,
method,
params,
expectedRuntimeId,
callbacks,
ensureReady: () => this.ensureReadyWithTimeout(timeoutMs),
sendSubscription: (subscription) => this.sendSubscription(subscription),
@@ -182,7 +182,7 @@ export class RemoteRuntimeSharedControlConnection {
onError: (error) => this.handleSocketClosed(error, socketGeneration),
onTextFrame: (frame) => this.handleTextFrame(frame, socketGeneration),
liveness: {
options: this.opts.liveness,
options: this.options.liveness,
onDead: (error) => this.handleSocketClosed(error, socketGeneration)
}
})
@@ -204,7 +204,7 @@ export class RemoteRuntimeSharedControlConnection {
frame,
state: this.state,
sharedKey: this.sharedKey,
environmentId: this.opts.environmentId,
environmentId: this.options.environmentId,
deviceToken: this.pairing.deviceToken,
pendingRequests: this.pendingRequests,
subscriptions: this.subscriptions,
@@ -217,7 +217,7 @@ export class RemoteRuntimeSharedControlConnection {
sendEncrypted: (payload) => this.sendEncrypted(payload),
markReady: () => {
this.lastConnectedAt = Date.now()
this.stableReset.schedule({
this.readyStableReset.schedule({
getState: () => this.state,
getSocket: () => this.ws,
reset: () => this.reconnect.resetAttempt()
@@ -301,7 +301,7 @@ export class RemoteRuntimeSharedControlConnection {
private closeSocket(error?: Error, preserveReadyWaitersAndPendingRequests = false): void {
closeSharedControlSocket({
environmentId: this.opts.environmentId,
environmentId: this.options.environmentId,
state: this.state,
pendingRequests: this.pendingRequests,
subscriptions: this.subscriptions,
@@ -311,7 +311,7 @@ export class RemoteRuntimeSharedControlConnection {
ws: this.ws,
error,
preserveReadyWaitersAndPendingRequests,
clearReadyStableTimer: () => this.stableReset.clear()
clearReadyStableTimer: () => this.readyStableReset.clear()
})
this.ws = this.sharedKey = null
this.socketCleanup = null
@@ -36,8 +36,7 @@ export function sendSharedControlSubscription(args: {
id: args.subscription.requestId,
deviceToken: args.deviceToken,
method: args.subscription.method,
params: args.subscription.params,
expectedRuntimeId: args.subscription.expectedRuntimeId
params: args.subscription.params
})
) {
args.subscription.sent = true
@@ -14,7 +14,6 @@ export async function startSharedControlSubscription<TResult>(args: {
deviceToken: string
method: string
params: unknown
expectedRuntimeId?: string
callbacks: SharedControlSubscriptionCallbacks<TResult>
ensureReady: () => Promise<void>
sendSubscription: (subscription: SharedControlLogicalSubscription<unknown>) => void
@@ -24,15 +23,13 @@ export async function startSharedControlSubscription<TResult>(args: {
subscriptions: args.subscriptions,
deviceToken: args.deviceToken,
method: args.method,
params: args.params,
expectedRuntimeId: args.expectedRuntimeId
params: args.params
})
const requestId = randomUUID()
const subscription = createSharedControlSubscription({
requestId,
method: args.method,
params: args.params,
expectedRuntimeId: args.expectedRuntimeId,
retainedParamsBytes,
callbacks: args.callbacks
})
@@ -1,6 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import type { RuntimeRpcResponse } from './runtime-rpc-envelope'
import { sendSharedControlSubscription } from './remote-runtime-shared-control-send'
import {
closeSharedControlLogicalSubscription,
createSharedControlSubscription,
@@ -34,39 +33,6 @@ function okResponse(subscriptionId: string): RuntimeRpcResponse<unknown> {
}
describe('closeSharedControlLogicalSubscription — replay-window leak', () => {
it('preserves the runtime fence when reconnect replay resends a subscription', () => {
const subscriptions = new Map<string, SharedControlLogicalSubscription<unknown>>()
const subscription = createSharedControlSubscription({
requestId: 'req-fenced',
method: 'runtime.clientEvents.subscribe',
params: null,
expectedRuntimeId: 'runtime-1',
retainedParamsBytes: 0,
callbacks: { onResponse: vi.fn(), onError: vi.fn() }
})
subscriptions.set(subscription.requestId, subscription)
const payloads: unknown[] = []
const send = (current: SharedControlLogicalSubscription<unknown>): void =>
sendSharedControlSubscription({
subscriptions,
subscription: current,
deviceToken: 'device-token',
send: (payload) => {
payloads.push(payload)
return true
}
})
send(subscription)
replaySharedControlSubscriptions({ subscriptions, send })
expect(payloads).toHaveLength(2)
expect(payloads).toEqual([
expect.objectContaining({ expectedRuntimeId: 'runtime-1' }),
expect.objectContaining({ expectedRuntimeId: 'runtime-1' })
])
})
it('sends the unsubscribe when closed after an established subscribe replay completes', () => {
const { subscriptions, subscription } = makeSubscriptions()
// First establishment: server assigned a concrete subscription id.
@@ -14,7 +14,6 @@ export function createSharedControlSubscription<TResult>(args: {
requestId: string
method: string
params: unknown
expectedRuntimeId?: string
retainedParamsBytes: number
callbacks: SharedControlSubscriptionCallbacks<TResult>
}): SharedControlLogicalSubscription<TResult> {
@@ -22,7 +21,6 @@ export function createSharedControlSubscription<TResult>(args: {
requestId: args.requestId,
method: args.method,
params: args.params,
expectedRuntimeId: args.expectedRuntimeId,
retainedParamsBytes: args.retainedParamsBytes,
callbacks: args.callbacks,
sent: false,
@@ -33,7 +33,6 @@ export type SharedControlLogicalSubscription<TResult = unknown> = {
requestId: string
method: string
params: unknown
expectedRuntimeId?: string
retainedParamsBytes: number
callbacks: SharedControlSubscriptionCallbacks<TResult>
sent: boolean
-2
View File
@@ -76,8 +76,6 @@ export type RuntimeRpcFailure = {
export type RuntimeRpcResponse<TResult> = RuntimeRpcSuccess<TResult> | RuntimeRpcFailure
export type RuntimeOrchestrationEnvelope = {
/** Reject before dispatch when the request reaches a replacement runtime. */
expectedRuntimeId?: string
orchestrationCapability?: string
orchestrationContractVersion?: number
orchestrationRequestId?: string