From 4882eeb8acf2992bbaa84de4eabebfd386c39f54 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:01:45 -0700 Subject: [PATCH] rm git shim: neutralize stale wrappers without a host gate (#14255) * Revert "fix terminal attribution shim removal edge cases (#14187)" This reverts 585dd6d3a9c. 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. --- .../app/h/[hostId]/session/[worktreeId].tsx | 57 ++-- .../session/ai-vault-resume-launch.test.ts | 81 +---- mobile/src/session/ai-vault-resume-launch.ts | 16 +- .../ai-vault-resume-preparation.test.ts | 15 +- ...mobile-terminal-attribution-compat.test.ts | 81 ----- .../mobile-terminal-attribution-compat.ts | 34 --- .../src/session/pr-ai-triage-launch.test.ts | 54 +--- mobile/src/session/pr-ai-triage-launch.ts | 27 +- ...se-mobile-diff-review-send-actions.test.ts | 68 ----- .../use-mobile-diff-review-send-actions.ts | 27 +- mobile/src/transport/rpc-client.ts | 11 +- mobile/src/transport/types.ts | 1 - src/cli/handlers/terminal.test.ts | 64 ---- src/cli/handlers/terminal.ts | 69 ++--- src/cli/index.test.ts | 284 +++++------------- src/cli/runtime/client.ts | 1 - src/cli/runtime/transport.test.ts | 42 --- src/cli/runtime/transport.ts | 1 - ...ntime-environment-connectivity-handlers.ts | 4 +- ...runtime-environment-request-connections.ts | 6 +- .../runtime-environment-transport-routing.ts | 12 +- src/main/ipc/runtime-environments.test.ts | 48 +-- src/main/ipc/runtime-environments.ts | 4 +- src/main/pty/legacy-terminal-shim-dir.test.ts | 60 +++- src/main/pty/legacy-terminal-shim-dir.ts | 53 +++- src/main/runtime/rpc/core.ts | 1 - .../rpc/dispatcher-computer-errors.test.ts | 22 +- src/main/runtime/rpc/dispatcher.ts | 21 -- .../rpc/methods/terminal-split-env.test.ts | 37 --- src/main/runtime/rpc/methods/terminal.ts | 2 - src/main/runtime/rpc/streaming.test.ts | 22 -- src/preload/api-types.ts | 2 - src/preload/index.ts | 2 - .../runtime-environment-subscriptions.test.ts | 9 +- .../runtime-environment-subscriptions.ts | 2 - .../terminal-pane/pty-transport.test.ts | 60 ++-- .../remote-runtime-pty-transport.test.ts | 191 ++---------- .../remote-runtime-pty-transport.ts | 121 ++------ ...ch-agent-background-session-remote.test.ts | 5 +- .../lib/runtime-agent-background-create.ts | 26 +- .../worktree-activation-created-agent.test.ts | 74 ++--- .../worktree-activation-empty-remote.test.ts | 65 ++-- .../abortable-runtime-environment-call.ts | 12 +- .../remote-agent-session-launch.test.ts | 138 ++++----- .../runtime/remote-agent-session-launch.ts | 71 ++--- .../runtime/runtime-environment-authority.ts | 46 --- ...untime-rpc-client-pairing-revision.test.ts | 60 +--- .../src/runtime/runtime-rpc-client.ts | 6 +- .../runtime-rpc-environment-call.test.ts | 28 -- .../runtime/runtime-rpc-environment-call.ts | 7 +- .../src/runtime/web-runtime-session.test.ts | 219 +++----------- .../src/runtime/web-runtime-session.ts | 98 ++---- .../legacy-terminal-attribution-env.test.ts | 99 ------ src/shared/legacy-terminal-attribution-env.ts | 68 ----- src/shared/protocol-version.ts | 4 - src/shared/remote-runtime-client.test.ts | 29 +- src/shared/remote-runtime-client.ts | 6 +- src/shared/remote-runtime-memory-limits.ts | 1 - ...remote-runtime-shared-control-admission.ts | 11 +- ...emote-runtime-shared-control-connection.ts | 22 +- .../remote-runtime-shared-control-send.ts | 3 +- ...ntime-shared-control-subscription-start.ts | 5 +- ...ntime-shared-control-subscriptions.test.ts | 34 --- ...te-runtime-shared-control-subscriptions.ts | 2 - .../remote-runtime-shared-control-types.ts | 1 - src/shared/runtime-rpc-envelope.ts | 2 - 66 files changed, 555 insertions(+), 2199 deletions(-) delete mode 100644 mobile/src/session/mobile-terminal-attribution-compat.test.ts delete mode 100644 mobile/src/session/mobile-terminal-attribution-compat.ts delete mode 100644 src/main/runtime/rpc/methods/terminal-split-env.test.ts delete mode 100644 src/renderer/src/runtime/runtime-environment-authority.ts delete mode 100644 src/renderer/src/runtime/runtime-rpc-environment-call.test.ts delete mode 100644 src/shared/legacy-terminal-attribution-env.test.ts delete mode 100644 src/shared/legacy-terminal-attribution-env.ts diff --git a/mobile/app/h/[hostId]/session/[worktreeId].tsx b/mobile/app/h/[hostId]/session/[worktreeId].tsx index 123a1212eb0..c0bcca64b98 100644 --- a/mobile/app/h/[hostId]/session/[worktreeId].tsx +++ b/mobile/app/h/[hostId]/session/[worktreeId].tsx @@ -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) } diff --git a/mobile/src/session/ai-vault-resume-launch.test.ts b/mobile/src/session/ai-vault-resume-launch.test.ts index 3e9af99191c..9efc0e50f1a 100644 --- a/mobile/src/session/ai-vault-resume-launch.test.ts +++ b/mobile/src/session/ai-vault-resume-launch.test.ts @@ -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', () => { diff --git a/mobile/src/session/ai-vault-resume-launch.ts b/mobile/src/session/ai-vault-resume-launch.ts index 6975b6f355d..aa8c1bf98a6 100644 --- a/mobile/src/session/ai-vault-resume-launch.ts +++ b/mobile/src/session/ai-vault-resume-launch.ts @@ -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([ 'aix', @@ -174,13 +169,12 @@ export async function resumeAiVaultSessionInTerminal( worktreeId: string, launch: MobileAiVaultResumeLaunch & { clientMutationId?: string } ): Promise { - 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') diff --git a/mobile/src/session/ai-vault-resume-preparation.test.ts b/mobile/src/session/ai-vault-resume-preparation.test.ts index 3e64a06a0f8..a0eed5515c6 100644 --- a/mobile/src/session/ai-vault-resume-preparation.test.ts +++ b/mobile/src/session/ai-vault-resume-preparation.test.ts @@ -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', diff --git a/mobile/src/session/mobile-terminal-attribution-compat.test.ts b/mobile/src/session/mobile-terminal-attribution-compat.test.ts deleted file mode 100644 index 9f0ebf873bd..00000000000 --- a/mobile/src/session/mobile-terminal-attribution-compat.test.ts +++ /dev/null @@ -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' - ) - }) -}) diff --git a/mobile/src/session/mobile-terminal-attribution-compat.ts b/mobile/src/session/mobile-terminal-attribution-compat.ts deleted file mode 100644 index ceb2d3f694e..00000000000 --- a/mobile/src/session/mobile-terminal-attribution-compat.ts +++ /dev/null @@ -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 -): Promise { - 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 }) -} diff --git a/mobile/src/session/pr-ai-triage-launch.test.ts b/mobile/src/session/pr-ai-triage-launch.test.ts index fe9271f5e4d..c2273c9046e 100644 --- a/mobile/src/session/pr-ai-triage-launch.test.ts +++ b/mobile/src/session/pr-ai-triage-launch.test.ts @@ -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) - }) }) diff --git a/mobile/src/session/pr-ai-triage-launch.ts b/mobile/src/session/pr-ai-triage-launch.ts index 1f764be1789..6d6c0bbc8b2 100644 --- a/mobile/src/session/pr-ai-triage-launch.ts +++ b/mobile/src/session/pr-ai-triage-launch.ts @@ -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 { - 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') } diff --git a/mobile/src/session/use-mobile-diff-review-send-actions.test.ts b/mobile/src/session/use-mobile-diff-review-send-actions.test.ts index 9ae2749412b..1d06190948c 100644 --- a/mobile/src/session/use-mobile-diff-review-send-actions.test.ts +++ b/mobile/src/session/use-mobile-diff-review-send-actions.test.ts @@ -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) diff --git a/mobile/src/session/use-mobile-diff-review-send-actions.ts b/mobile/src/session/use-mobile-diff-review-send-actions.ts index 266caaf6879..0867a3884b3 100644 --- a/mobile/src/session/use-mobile-diff-review-send-actions.ts +++ b/mobile/src/session/use-mobile-diff-review-send-actions.ts @@ -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') } diff --git a/mobile/src/transport/rpc-client.ts b/mobile/src/transport/rpc-client.ts index f316f8c2b76..9b0afe937f3 100644 --- a/mobile/src/transport/rpc-client.ts +++ b/mobile/src/transport/rpc-client.ts @@ -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')) diff --git a/mobile/src/transport/types.ts b/mobile/src/transport/types.ts index 8fe2bdd78f9..9d96f0561ad 100644 --- a/mobile/src/transport/types.ts +++ b/mobile/src/transport/types.ts @@ -18,7 +18,6 @@ export type RpcRequest = { deviceToken: string method: string params?: unknown - expectedRuntimeId?: string } export type RpcSuccess = { diff --git a/src/cli/handlers/terminal.test.ts b/src/cli/handlers/terminal.test.ts index 8c5ff460c77..7dadf41c2d3 100644 --- a/src/cli/handlers/terminal.test.ts +++ b/src/cli/handlers/terminal.test.ts @@ -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()) - }) -}) diff --git a/src/cli/handlers/terminal.ts b/src/cli/handlers/terminal.ts index 677cdc81034..551ab8d95c1 100644 --- a/src/cli/handlers/terminal.ts +++ b/src/cli/handlers/terminal.ts @@ -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 = { const useRendererBackedInteractiveTerminal = !client.isRemote && shouldUseRendererBackedInteractiveTerminal(command) const focus = flags.get('focus') === true - const status = await client.call('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 = { ) { throw new RuntimeClientError('invalid_argument', '--direction must be horizontal or vertical') } - const status = await client.call('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) } } diff --git a/src/cli/index.test.ts b/src/cli/index.test.ts index 2f86ee9583f..4867aebc29b 100644 --- a/src/cli/index.test.ts +++ b/src/cli/index.test.ts @@ -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 () => { diff --git a/src/cli/runtime/client.ts b/src/cli/runtime/client.ts index b6c49ff7c22..3a8951de554 100644 --- a/src/cli/runtime/client.ts +++ b/src/cli/runtime/client.ts @@ -87,7 +87,6 @@ export class RuntimeClient { } : {} const envelope = { - expectedRuntimeId: options?.expectedRuntimeId, orchestrationCapability: options?.orchestrationCapability, orchestrationContractVersion: method.startsWith('orchestration.') ? ORCHESTRATION_CONTRACT_VERSION diff --git a/src/cli/runtime/transport.test.ts b/src/cli/runtime/transport.test.ts index 7858d6cd131..8de35f619e5 100644 --- a/src/cli/runtime/transport.test.ts +++ b/src/cli/runtime/transport.test.ts @@ -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 | 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 - 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((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') diff --git a/src/cli/runtime/transport.ts b/src/cli/runtime/transport.ts index 998052dc4b6..091ca9da0e0 100644 --- a/src/cli/runtime/transport.ts +++ b/src/cli/runtime/transport.ts @@ -186,7 +186,6 @@ export async function sendRequest( authToken: metadata.authToken, method, params, - expectedRuntimeId: envelope?.expectedRuntimeId, orchestrationCapability: envelope?.orchestrationCapability, orchestrationContractVersion: envelope?.orchestrationContractVersion, orchestrationRequestId: envelope?.orchestrationRequestId, diff --git a/src/main/ipc/runtime-environment-connectivity-handlers.ts b/src/main/ipc/runtime-environment-connectivity-handlers.ts index 2c4d1462540..9d3732f886b 100644 --- a/src/main/ipc/runtime-environment-connectivity-handlers.ts +++ b/src/main/ipc/runtime-environment-connectivity-handlers.ts @@ -183,7 +183,6 @@ function registerPassiveCallHandler(getUserDataPath: () => string): void { params?: unknown timeoutMs?: number expectedEnvironmentPairingRevision?: number - expectedRuntimeId?: string } ): Promise> => { 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) diff --git a/src/main/ipc/runtime-environment-request-connections.ts b/src/main/ipc/runtime-environment-request-connections.ts index c9782d61772..58fb95dd6e1 100644 --- a/src/main/ipc/runtime-environment-request-connections.ts +++ b/src/main/ipc/runtime-environment-request-connections.ts @@ -77,15 +77,13 @@ export function subscribeRemoteRuntimeSharedControlRequest( onBinary?: (bytes: Uint8Array) => void onError: (error: { code: string; message: string }) => void onClose?: () => void - }, - envelope?: RuntimeOrchestrationEnvelope + } ): Promise { return getSharedControlConnection(environmentId, pairing).subscribe( method, params, timeoutMs, - callbacks, - envelope?.expectedRuntimeId + callbacks ) } diff --git a/src/main/ipc/runtime-environment-transport-routing.ts b/src/main/ipc/runtime-environment-transport-routing.ts index 0792a1ea009..884b0b2874a 100644 --- a/src/main/ipc/runtime-environment-transport-routing.ts +++ b/src/main/ipc/runtime-environment-transport-routing.ts @@ -193,14 +193,11 @@ export async function subscribeRuntimeEnvironment( | { type: 'close' } ) => void onClose: () => void - }, - expectedRuntimeId?: string + } ): Promise { 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) { diff --git a/src/main/ipc/runtime-environments.test.ts b/src/main/ipc/runtime-environments.test.ts index b351b88681a..9ca8da98600 100644 --- a/src/main/ipc/runtime-environments.test.ts +++ b/src/main/ipc/runtime-environments.test.ts @@ -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' }), diff --git a/src/main/ipc/runtime-environments.ts b/src/main/ipc/runtime-environments.ts index 3ddf4e2aa6c..e31f8569dec 100644 --- a/src/main/ipc/runtime-environments.ts +++ b/src/main/ipc/runtime-environments.ts @@ -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() diff --git a/src/main/pty/legacy-terminal-shim-dir.test.ts b/src/main/pty/legacy-terminal-shim-dir.test.ts index 32e59800266..4d526b8381d 100644 --- a/src/main/pty/legacy-terminal-shim-dir.test.ts +++ b/src/main/pty/legacy-terminal-shim-dir.test.ts @@ -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') diff --git a/src/main/pty/legacy-terminal-shim-dir.ts b/src/main/pty/legacy-terminal-shim-dir.ts index 0d64ebaed05..94a4383b25b 100644 --- a/src/main/pty/legacy-terminal-shim-dir.ts +++ b/src/main/pty/legacy-terminal-shim-dir.ts @@ -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. + } } } diff --git a/src/main/runtime/rpc/core.ts b/src/main/runtime/rpc/core.ts index fa3c812ce82..b415ddd0e71 100644 --- a/src/main/runtime/rpc/core.ts +++ b/src/main/runtime/rpc/core.ts @@ -46,7 +46,6 @@ export type RpcRequest = { authToken: string method: string params?: unknown - expectedRuntimeId?: string orchestrationCapability?: string orchestrationContractVersion?: number orchestrationRequestId?: string diff --git a/src/main/runtime/rpc/dispatcher-computer-errors.test.ts b/src/main/runtime/rpc/dispatcher-computer-errors.test.ts index 10bc94b771b..862973e3913 100644 --- a/src/main/runtime/rpc/dispatcher-computer-errors.test.ts +++ b/src/main/runtime/rpc/dispatcher-computer-errors.test.ts @@ -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 }) diff --git a/src/main/runtime/rpc/dispatcher.ts b/src/main/runtime/rpc/dispatcher.ts index 995d0f00d64..be11b2b0c64 100644 --- a/src/main/runtime/rpc/dispatcher.ts +++ b/src/main/runtime/rpc/dispatcher.ts @@ -43,14 +43,6 @@ export class RpcDispatcher { async dispatch(request: RpcRequest, options?: { signal?: AbortSignal }): Promise { 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 { 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( diff --git a/src/main/runtime/rpc/methods/terminal-split-env.test.ts b/src/main/runtime/rpc/methods/terminal-split-env.test.ts deleted file mode 100644 index 3bbf1291f00..00000000000 --- a/src/main/runtime/rpc/methods/terminal-split-env.test.ts +++ /dev/null @@ -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 - }) - }) -}) diff --git a/src/main/runtime/rpc/methods/terminal.ts b/src/main/runtime/rpc/methods/terminal.ts index 32c31149f3f..69f4eb72190 100644 --- a/src/main/runtime/rpc/methods/terminal.ts +++ b/src/main/runtime/rpc/methods/terminal.ts @@ -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 }) }) diff --git a/src/main/runtime/rpc/streaming.test.ts b/src/main/runtime/rpc/streaming.test.ts index 4b4ce8a9f21..382353de803 100644 --- a/src/main/runtime/rpc/streaming.test.ts +++ b/src/main/runtime/rpc/streaming.test.ts @@ -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({ diff --git a/src/preload/api-types.ts b/src/preload/api-types.ts index 54e03a81f58..266d659ba98 100644 --- a/src/preload/api-types.ts +++ b/src/preload/api-types.ts @@ -3415,7 +3415,6 @@ export type PreloadApi = { params?: unknown timeoutMs?: number expectedEnvironmentPairingRevision?: number - expectedRuntimeId?: string }) => Promise> subscribe: ( args: { @@ -3424,7 +3423,6 @@ export type PreloadApi = { params?: unknown timeoutMs?: number expectedEnvironmentPairingRevision?: number - expectedRuntimeId?: string }, callbacks: { onResponse: (response: RuntimeRpcResponse) => void diff --git a/src/preload/index.ts b/src/preload/index.ts index 4808dd3b979..31e0c5cbeeb 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -4429,7 +4429,6 @@ const api = { params?: unknown timeoutMs?: number expectedEnvironmentPairingRevision?: number - expectedRuntimeId?: string }): Promise> => 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) => void diff --git a/src/preload/runtime-environment-subscriptions.test.ts b/src/preload/runtime-environment-subscriptions.test.ts index a77614e1196..585b0f75fa9 100644 --- a/src/preload/runtime-environment-subscriptions.test.ts +++ b/src/preload/runtime-environment-subscriptions.test.ts @@ -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' }) diff --git a/src/preload/runtime-environment-subscriptions.ts b/src/preload/runtime-environment-subscriptions.ts index c804c651f73..f9d46b467aa 100644 --- a/src/preload/runtime-environment-subscriptions.ts +++ b/src/preload/runtime-environment-subscriptions.ts @@ -5,8 +5,6 @@ type RuntimeEnvironmentSubscribeArgs = { method: string params?: unknown timeoutMs?: number - expectedEnvironmentPairingRevision?: number - expectedRuntimeId?: string } type RuntimeEnvironmentSubscriptionCallbacks = { diff --git a/src/renderer/src/components/terminal-pane/pty-transport.test.ts b/src/renderer/src/components/terminal-pane/pty-transport.test.ts index 1c3c5b1269c..3ab835b11a4 100644 --- a/src/renderer/src/components/terminal-pane/pty-transport.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-transport.test.ts @@ -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( diff --git a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts index c160188f64d..4087cf59a15 100644 --- a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts +++ b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts @@ -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' } } }) } diff --git a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts index 35189447c57..6eff59d84ce 100644 --- a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts +++ b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts @@ -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 { const response = await window.api.runtimeEnvironments.call({ selector: environmentId, method, params, timeoutMs, - expectedEnvironmentPairingRevision: runtimeEnvironmentPairingRevision, - ...(expectedRuntimeId ? { expectedRuntimeId } : {}) + expectedEnvironmentPairingRevision: runtimeEnvironmentPairingRevision }) return unwrapRuntimeRpcResult(response as RuntimeRpcResponse) } @@ -924,8 +908,7 @@ export function createRemoteRuntimePtyTransport( reconcileExisting: boolean ) => Promise, environmentId: string, - expectedLifecycleEpoch: number, - beforeReplay?: (timeoutMs: number) => Promise + expectedLifecycleEpoch: number ): Promise { 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 => { - const status = await callRuntimeForEnvironment( - 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( 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({ - environmentId: createEnvironmentId, - expectedEnvironmentPairingRevision: runtimeEnvironmentPairingRevision, - hostAuthority: hostAuthorityCreate, - requiredHostAuthorityCapabilities: requiredAgentSessionCapabilities, - legacy: legacyCreate - }) + ? await hostAuthorityCreate() : await runRemoteAgentSessionLaunch({ environmentId: createEnvironmentId, - expectedEnvironmentPairingRevision: runtimeEnvironmentPairingRevision, hostAuthority: hostAuthorityCreate, - requiredHostAuthorityCapabilities: requiredAgentSessionCapabilities, + ...(resumeProviderSessionToSend && launchAgentToSend === 'omp' + ? { hostAuthorityCapability: AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY } + : {}), legacy: legacyCreate }) - : await runRemoteAgentSessionLaunch({ - 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() diff --git a/src/renderer/src/lib/launch-agent-background-session-remote.test.ts b/src/renderer/src/lib/launch-agent-background-session-remote.test.ts index 4066f67522f..6cf8e9b717d 100644 --- a/src/renderer/src/lib/launch-agent-background-session-remote.test.ts +++ b/src/renderer/src/lib/launch-agent-background-session-remote.test.ts @@ -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', diff --git a/src/renderer/src/lib/runtime-agent-background-create.ts b/src/renderer/src/lib/runtime-agent-background-create.ts index a6c430f172b..70915ae63d4 100644 --- a/src/renderer/src/lib/runtime-agent-background-create.ts +++ b/src/renderer/src/lib/runtime-agent-background-create.ts @@ -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 } ) }) } diff --git a/src/renderer/src/lib/worktree-activation-created-agent.test.ts b/src/renderer/src/lib/worktree-activation-created-agent.test.ts index e8a1ef3f052..cc2af6e7d7c 100644 --- a/src/renderer/src/lib/worktree-activation-created-agent.test.ts +++ b/src/renderer/src/lib/worktree-activation-created-agent.test.ts @@ -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' diff --git a/src/renderer/src/lib/worktree-activation-empty-remote.test.ts b/src/renderer/src/lib/worktree-activation-empty-remote.test.ts index c4e2edc4907..49f38e7aa7c 100644 --- a/src/renderer/src/lib/worktree-activation-empty-remote.test.ts +++ b/src/renderer/src/lib/worktree-activation-empty-remote.test.ts @@ -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', diff --git a/src/renderer/src/runtime/abortable-runtime-environment-call.ts b/src/renderer/src/runtime/abortable-runtime-environment-call.ts index 357bf1b2d08..619dfc517ce 100644 --- a/src/renderer/src/runtime/abortable-runtime-environment-call.ts +++ b/src/renderer/src/runtime/abortable-runtime-environment-call.ts @@ -12,8 +12,7 @@ export async function callAbortableRuntimeEnvironment( params: unknown, timeoutMs: number | undefined, signal: AbortSignal, - expectedEnvironmentPairingRevision?: number, - expectedRuntimeId?: string + expectedEnvironmentPairingRevision?: number ): Promise> { 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))), diff --git a/src/renderer/src/runtime/remote-agent-session-launch.test.ts b/src/renderer/src/runtime/remote-agent-session-launch.test.ts index aea62019626..0fb6399d688 100644 --- a/src/renderer/src/runtime/remote-agent-session-launch.test.ts +++ b/src/renderer/src/runtime/remote-agent-session-launch.test.ts @@ -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() }) }) diff --git a/src/renderer/src/runtime/remote-agent-session-launch.ts b/src/renderer/src/runtime/remote-agent-session-launch.ts index 82c61d89ab1..b00c6cbbdab 100644 --- a/src/renderer/src/runtime/remote-agent-session-launch.ts +++ b/src/renderer/src/runtime/remote-agent-session-launch.ts @@ -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(args: { environmentId: string - expectedEnvironmentPairingRevision?: number - hostAuthority?: (authority: LiveRuntimeEnvironmentAuthority) => Promise - requiredHostAuthorityCapabilities?: readonly RuntimeCapability[] - legacy: (options: { - skipCompatibilityCheck: boolean - authority: LiveRuntimeEnvironmentAuthority - }) => Promise + hostAuthority?: () => Promise + hostAuthorityCapability?: RuntimeCapability + legacy: (options: { skipCompatibilityCheck: boolean }) => Promise }): Promise { - const requiredCapabilities = [ - TERMINAL_ATTRIBUTION_REMOVED_RUNTIME_CAPABILITY, - ...(args.hostAuthority ? [AGENT_SESSION_HOST_AUTHORITY_CAPABILITY] : []), - ...(args.requiredHostAuthorityCapabilities ?? []) - ] - let probe: Awaited> + 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 } diff --git a/src/renderer/src/runtime/runtime-environment-authority.ts b/src/renderer/src/runtime/runtime-environment-authority.ts deleted file mode 100644 index f773941bdf3..00000000000 --- a/src/renderer/src/runtime/runtime-environment-authority.ts +++ /dev/null @@ -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( - response as RuntimeRpcResponse - ) - 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]) - }) - } -} diff --git a/src/renderer/src/runtime/runtime-rpc-client-pairing-revision.test.ts b/src/renderer/src/runtime/runtime-rpc-client-pairing-revision.test.ts index 00995d0db6d..b3e4c103f4f 100644 --- a/src/renderer/src/runtime/runtime-rpc-client-pairing-revision.test.ts +++ b/src/renderer/src/runtime/runtime-rpc-client-pairing-revision.test.ts @@ -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 - }) -}) diff --git a/src/renderer/src/runtime/runtime-rpc-client.ts b/src/renderer/src/runtime/runtime-rpc-client.ts index ad79ba94f13..7819314e5b0 100644 --- a/src/renderer/src/runtime/runtime-rpc-client.ts +++ b/src/renderer/src/runtime/runtime-rpc-client.ts @@ -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( skipCompatibilityCheck?: boolean signal?: AbortSignal expectedEnvironmentPairingRevision?: number - expectedRuntimeId?: string } = {} ): Promise { const expectedEnvironmentPairingRevision = @@ -91,8 +88,7 @@ export async function callRuntimeRpc( params: nextParams, timeoutMs: options.timeoutMs, signal: options.signal, - expectedEnvironmentPairingRevision, - expectedRuntimeId: options.expectedRuntimeId + expectedEnvironmentPairingRevision }) return unwrapRuntimeRpcResult(response as RuntimeRpcResponse) } diff --git a/src/renderer/src/runtime/runtime-rpc-environment-call.test.ts b/src/renderer/src/runtime/runtime-rpc-environment-call.test.ts deleted file mode 100644 index 3f8b8c53c5c..00000000000 --- a/src/renderer/src/runtime/runtime-rpc-environment-call.test.ts +++ /dev/null @@ -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() -}) diff --git a/src/renderer/src/runtime/runtime-rpc-environment-call.ts b/src/renderer/src/runtime/runtime-rpc-environment-call.ts index dc5f85f8a62..5acc0fa0a85 100644 --- a/src/renderer/src/runtime/runtime-rpc-environment-call.ts +++ b/src/renderer/src/runtime/runtime-rpc-environment-call.ts @@ -7,7 +7,6 @@ export async function callRuntimeEnvironmentWithRevision(args: { timeoutMs?: number signal?: AbortSignal expectedEnvironmentPairingRevision?: number - expectedRuntimeId?: string }): Promise { 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 }) } diff --git a/src/renderer/src/runtime/web-runtime-session.test.ts b/src/renderer/src/runtime/web-runtime-session.test.ts index 3e64191dcab..ba309a39f91 100644 --- a/src/renderer/src/runtime/web-runtime-session.test.ts +++ b/src/renderer/src/runtime/web-runtime-session.test.ts @@ -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)) }) }) diff --git a/src/renderer/src/runtime/web-runtime-session.ts b/src/renderer/src/runtime/web-runtime-session.ts index 089ceafd53c..34dcfae1d3a 100644 --- a/src/renderer/src/runtime/web-runtime-session.ts +++ b/src/renderer/src/runtime/web-runtime-session.ts @@ -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> { 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 => { - const response = await callEnvironment({ method: 'status.get', timeoutMs: 15_000 }) - const status = unwrapRuntimeRpcResult(response as RuntimeRpcResponse) - 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 ) : 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 ) ) @@ -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 @@ -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 @@ -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) - 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) }) diff --git a/src/shared/legacy-terminal-attribution-env.test.ts b/src/shared/legacy-terminal-attribution-env.test.ts deleted file mode 100644 index 1bb84cc5f96..00000000000 --- a/src/shared/legacy-terminal-attribution-env.test.ts +++ /dev/null @@ -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) - }) -}) diff --git a/src/shared/legacy-terminal-attribution-env.ts b/src/shared/legacy-terminal-attribution-env.ts deleted file mode 100644 index 5237280a486..00000000000 --- a/src/shared/legacy-terminal-attribution-env.ts +++ /dev/null @@ -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 | undefined -): Record { - 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] -} diff --git a/src/shared/protocol-version.ts b/src/shared/protocol-version.ts index 28a89f9814b..eb61be79c97 100644 --- a/src/shared/protocol-version.ts +++ b/src/shared/protocol-version.ts @@ -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 diff --git a/src/shared/remote-runtime-client.test.ts b/src/shared/remote-runtime-client.test.ts index 4c3bfba011e..9ead47f4067 100644 --- a/src/shared/remote-runtime-client.test.ts +++ b/src/shared/remote-runtime-client.test.ts @@ -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 nextAuth: Promise - nextRequest: Promise> }> { const serverKeyPair = generateKeyPair() let resolveBinary: (bytes: Uint8Array) => void = () => {} @@ -365,10 +345,6 @@ async function createSubscriptionServer( const nextAuth = new Promise((resolve) => { resolveAuth = resolve }) - let resolveRequest: (request: Record) => void = () => {} - const nextRequest = new Promise>((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 - 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 { diff --git a/src/shared/remote-runtime-client.ts b/src/shared/remote-runtime-client.ts index 3b308724b57..c53565dc8a2 100644 --- a/src/shared/remote-runtime-client.ts +++ b/src/shared/remote-runtime-client.ts @@ -497,16 +497,14 @@ export async function subscribeRemoteRuntimeRequest( params: unknown, timeoutMs: number, callbacks: RemoteRuntimeSubscriptionCallbacks, - livenessOptions?: RemoteRuntimeSocketLivenessOptions, - envelope?: RuntimeOrchestrationEnvelope + livenessOptions?: RemoteRuntimeSocketLivenessOptions ): Promise { const requestId = randomUUID() const serializedRequest = serializeRemoteRuntimeRpcRequest({ requestId, deviceToken: pairing.deviceToken, method, - params, - envelope + params }) const serializedAuth = serializeRemoteRuntimePayload({ type: 'e2ee_auth', diff --git a/src/shared/remote-runtime-memory-limits.ts b/src/shared/remote-runtime-memory-limits.ts index 8b5ce04cc56..a41409ffdfe 100644 --- a/src/shared/remote-runtime-memory-limits.ts +++ b/src/shared/remote-runtime-memory-limits.ts @@ -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, diff --git a/src/shared/remote-runtime-shared-control-admission.ts b/src/shared/remote-runtime-shared-control-admission.ts index f6638ed7f63..e526ba1bb95 100644 --- a/src/shared/remote-runtime-shared-control-admission.ts +++ b/src/shared/remote-runtime-shared-control-admission.ts @@ -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 }) } diff --git a/src/shared/remote-runtime-shared-control-connection.ts b/src/shared/remote-runtime-shared-control-connection.ts index 93925ccf15e..4fb40525245 100644 --- a/src/shared/remote-runtime-shared-control-connection.ts +++ b/src/shared/remote-runtime-shared-control-connection.ts @@ -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( @@ -83,15 +85,13 @@ export class RemoteRuntimeSharedControlConnection { method: string, params: unknown, timeoutMs: number, - callbacks: SharedControlSubscriptionCallbacks, - expectedRuntimeId?: string + callbacks: SharedControlSubscriptionCallbacks ): Promise { 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 diff --git a/src/shared/remote-runtime-shared-control-send.ts b/src/shared/remote-runtime-shared-control-send.ts index 1862751cd6b..2a7ff16cfa5 100644 --- a/src/shared/remote-runtime-shared-control-send.ts +++ b/src/shared/remote-runtime-shared-control-send.ts @@ -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 diff --git a/src/shared/remote-runtime-shared-control-subscription-start.ts b/src/shared/remote-runtime-shared-control-subscription-start.ts index 1cc4a91a5a8..2b22aaabc5b 100644 --- a/src/shared/remote-runtime-shared-control-subscription-start.ts +++ b/src/shared/remote-runtime-shared-control-subscription-start.ts @@ -14,7 +14,6 @@ export async function startSharedControlSubscription(args: { deviceToken: string method: string params: unknown - expectedRuntimeId?: string callbacks: SharedControlSubscriptionCallbacks ensureReady: () => Promise sendSubscription: (subscription: SharedControlLogicalSubscription) => void @@ -24,15 +23,13 @@ export async function startSharedControlSubscription(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 }) diff --git a/src/shared/remote-runtime-shared-control-subscriptions.test.ts b/src/shared/remote-runtime-shared-control-subscriptions.test.ts index b61b2eff34e..de5d5718b55 100644 --- a/src/shared/remote-runtime-shared-control-subscriptions.test.ts +++ b/src/shared/remote-runtime-shared-control-subscriptions.test.ts @@ -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 { } describe('closeSharedControlLogicalSubscription — replay-window leak', () => { - it('preserves the runtime fence when reconnect replay resends a subscription', () => { - const subscriptions = new Map>() - 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): 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. diff --git a/src/shared/remote-runtime-shared-control-subscriptions.ts b/src/shared/remote-runtime-shared-control-subscriptions.ts index 92da86bcd5d..4a93076d23f 100644 --- a/src/shared/remote-runtime-shared-control-subscriptions.ts +++ b/src/shared/remote-runtime-shared-control-subscriptions.ts @@ -14,7 +14,6 @@ export function createSharedControlSubscription(args: { requestId: string method: string params: unknown - expectedRuntimeId?: string retainedParamsBytes: number callbacks: SharedControlSubscriptionCallbacks }): SharedControlLogicalSubscription { @@ -22,7 +21,6 @@ export function createSharedControlSubscription(args: { requestId: args.requestId, method: args.method, params: args.params, - expectedRuntimeId: args.expectedRuntimeId, retainedParamsBytes: args.retainedParamsBytes, callbacks: args.callbacks, sent: false, diff --git a/src/shared/remote-runtime-shared-control-types.ts b/src/shared/remote-runtime-shared-control-types.ts index 592fdc47168..c6e55134762 100644 --- a/src/shared/remote-runtime-shared-control-types.ts +++ b/src/shared/remote-runtime-shared-control-types.ts @@ -33,7 +33,6 @@ export type SharedControlLogicalSubscription = { requestId: string method: string params: unknown - expectedRuntimeId?: string retainedParamsBytes: number callbacks: SharedControlSubscriptionCallbacks sent: boolean diff --git a/src/shared/runtime-rpc-envelope.ts b/src/shared/runtime-rpc-envelope.ts index 38cbc4659b1..d407de72cb9 100644 --- a/src/shared/runtime-rpc-envelope.ts +++ b/src/shared/runtime-rpc-envelope.ts @@ -76,8 +76,6 @@ export type RuntimeRpcFailure = { export type RuntimeRpcResponse = RuntimeRpcSuccess | RuntimeRpcFailure export type RuntimeOrchestrationEnvelope = { - /** Reject before dispatch when the request reaches a replacement runtime. */ - expectedRuntimeId?: string orchestrationCapability?: string orchestrationContractVersion?: number orchestrationRequestId?: string