From ae627c23eca865450b4a86cd5ba12a313e9a0f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 7 Oct 2026 22:45:36 +0200 Subject: [PATCH 1/4] refactor(apple-runner): drop the discarded uninstall-output classifier isBenignSimulatorRunnerUninstallResult sniffed simctl output, and both branches of its verdict returned without acting on it. Co-Authored-By: Claude Fable 5.1 --- .../platform-apple/src/runner/runner-session.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/packages/platform-apple/src/runner/runner-session.ts b/packages/platform-apple/src/runner/runner-session.ts index 27c9944cc5..e087337db9 100644 --- a/packages/platform-apple/src/runner/runner-session.ts +++ b/packages/platform-apple/src/runner/runner-session.ts @@ -521,11 +521,8 @@ async function cleanupStaleSimulatorRunnerBundles(device: DeviceInfo): Promise { - const result = await uninstallStaleSimulatorRunnerBundle(device, bundleId); - if (!result || isBenignSimulatorRunnerUninstallResult(result)) { - return; - } // Best-effort cleanup only; xcodebuild may still be able to install. + await uninstallStaleSimulatorRunnerBundle(device, bundleId); }), ); } @@ -554,18 +551,6 @@ async function uninstallStaleSimulatorRunnerBundle( } } -function isBenignSimulatorRunnerUninstallResult(result: ExecResult): boolean { - if (result.exitCode === 0) return true; - const output = `${result.stdout}\n${result.stderr}`.toLowerCase(); - return ( - output.includes('not installed') || - output.includes('found nothing') || - output.includes('no such file') || - output.includes('invalid device') || - output.includes('could not find') - ); -} - /** * The one reader of what is registered for a device: the session's state plus the one fact the * session cannot know itself — whether its runner process is still there. `null` means nothing is From 75f35141045ebd5f2a12a654c8db39eb8aceab9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 7 Oct 2026 22:10:00 +0200 Subject: [PATCH 2/4] refactor(contracts): bind direct local lifecycles once instead of per platform Web, Linux, Vega, and HarmonyOS each carried a 30-line lifecycle.ts that differed only in the owner label and open-target identity. Co-Authored-By: Claude Fable 5.1 --- .../src/application-lifecycle-interaction.ts | 24 +++++++++++++++ packages/platform-harmonyos/src/lifecycle.ts | 30 ------------------- packages/platform-harmonyos/src/runtime.ts | 6 ++-- packages/platform-linux/src/lifecycle.ts | 30 ------------------- packages/platform-linux/src/runtime.ts | 6 ++-- packages/platform-vega/src/lifecycle.ts | 30 ------------------- packages/platform-vega/src/runtime.ts | 6 ++-- packages/platform-web/src/lifecycle.ts | 30 ------------------- packages/platform-web/src/runtime.ts | 10 +++++-- 9 files changed, 44 insertions(+), 128 deletions(-) delete mode 100644 packages/platform-harmonyos/src/lifecycle.ts delete mode 100644 packages/platform-linux/src/lifecycle.ts delete mode 100644 packages/platform-vega/src/lifecycle.ts delete mode 100644 packages/platform-web/src/lifecycle.ts diff --git a/packages/contracts/src/application-lifecycle-interaction.ts b/packages/contracts/src/application-lifecycle-interaction.ts index 0db4ce162f..d179cbe202 100644 --- a/packages/contracts/src/application-lifecycle-interaction.ts +++ b/packages/contracts/src/application-lifecycle-interaction.ts @@ -380,3 +380,27 @@ export async function invokeApplicationClose( } await interactor.close(app); } + +/** + * The lifecycle binding of an owner whose interactor drives open and close directly: resolve the + * local interactor for the device, then dispatch through {@link bindDirectApplicationLifecycle}. + */ +export function bindLocalDirectApplicationLifecycle( + params: Readonly<{ + owner: string; + openTargetIdentity: DirectOpenTargetIdentity; + host: Readonly<{ resolve: LocalApplicationLifecycleInteractorResolver }>; + device: DeviceInfo; + signal: AbortSignal; + }>, +): ApplicationLifecycleRuntimeOperations { + return bindDirectApplicationLifecycle({ + owner: params.owner, + openTargetIdentity: params.openTargetIdentity, + binding: bindLocalApplicationLifecycleInteractor({ + device: params.device, + signal: params.signal, + resolveInteractor: params.host.resolve, + }), + }); +} diff --git a/packages/platform-harmonyos/src/lifecycle.ts b/packages/platform-harmonyos/src/lifecycle.ts deleted file mode 100644 index 57a4e378f2..0000000000 --- a/packages/platform-harmonyos/src/lifecycle.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { - ApplicationLifecycleRuntimeOperations, - LocalApplicationInteractorHost, -} from '@agent-device/contracts/application-lifecycle-runtime'; -import { - bindDirectApplicationLifecycle, - bindLocalApplicationLifecycleInteractor, -} from '@agent-device/contracts/application-lifecycle-interaction'; -import type { DeviceInfo } from '@agent-device/kernel/device'; - -type HarmonyLifecycleParams = Readonly<{ - host: LocalApplicationInteractorHost; - device: DeviceInfo; - signal: AbortSignal; -}>; - -/** HarmonyOS owns bundle selection and direct lifecycle dispatch after local binding. */ -export function bindHarmonyApplicationLifecycle( - params: HarmonyLifecycleParams, -): ApplicationLifecycleRuntimeOperations { - return bindDirectApplicationLifecycle({ - owner: 'HarmonyOS', - openTargetIdentity: 'bundle-id', - binding: bindLocalApplicationLifecycleInteractor({ - device: params.device, - signal: params.signal, - resolveInteractor: params.host.resolve, - }), - }); -} diff --git a/packages/platform-harmonyos/src/runtime.ts b/packages/platform-harmonyos/src/runtime.ts index e3ece28fa2..35e70c593e 100644 --- a/packages/platform-harmonyos/src/runtime.ts +++ b/packages/platform-harmonyos/src/runtime.ts @@ -65,7 +65,7 @@ import { harmonyScreenRecordingFacts, } from './recording/runtime.ts'; import { readHarmonyAppState } from './app-state.ts'; -import { bindHarmonyApplicationLifecycle } from './lifecycle.ts'; +import { bindLocalDirectApplicationLifecycle } from '@agent-device/contracts/application-lifecycle-interaction'; import { createHarmonyAppDeploymentOperations, harmonyAppDeploymentFacts, @@ -411,7 +411,9 @@ export function createHarmonyPlatformRuntime(host: PlatformRuntimeHost): Platfor ).map((app) => ({ id: app.package, name: app.name })); }, ...availableApplicationLifecycleOperations( - bindHarmonyApplicationLifecycle({ + bindLocalDirectApplicationLifecycle({ + owner: 'HarmonyOS', + openTargetIdentity: 'bundle-id', host: host.localInteractors, device: request.device, signal: request.scope.signal, diff --git a/packages/platform-linux/src/lifecycle.ts b/packages/platform-linux/src/lifecycle.ts deleted file mode 100644 index b35475a612..0000000000 --- a/packages/platform-linux/src/lifecycle.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { - ApplicationLifecycleRuntimeOperations, - LocalApplicationInteractorHost, -} from '@agent-device/contracts/application-lifecycle-runtime'; -import { - bindDirectApplicationLifecycle, - bindLocalApplicationLifecycleInteractor, -} from '@agent-device/contracts/application-lifecycle-interaction'; -import type { DeviceInfo } from '@agent-device/kernel/device'; - -type LinuxLifecycleParams = Readonly<{ - host: LocalApplicationInteractorHost; - device: DeviceInfo; - signal: AbortSignal; -}>; - -/** Linux owns its desktop lifecycle behavior after the local owner is bound. */ -export function bindLinuxApplicationLifecycle( - params: LinuxLifecycleParams, -): ApplicationLifecycleRuntimeOperations { - return bindDirectApplicationLifecycle({ - owner: 'Linux', - openTargetIdentity: 'app-name', - binding: bindLocalApplicationLifecycleInteractor({ - device: params.device, - signal: params.signal, - resolveInteractor: params.host.resolve, - }), - }); -} diff --git a/packages/platform-linux/src/runtime.ts b/packages/platform-linux/src/runtime.ts index 78f6648cdc..603ec8fc71 100644 --- a/packages/platform-linux/src/runtime.ts +++ b/packages/platform-linux/src/runtime.ts @@ -38,7 +38,7 @@ import { typeTextRuntimeOperationFacts } from '@agent-device/contracts/type-text import { touchRuntimeOperationFacts } from '@agent-device/contracts/touch-runtime'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; -import { bindLinuxApplicationLifecycle } from './lifecycle.ts'; +import { bindLocalDirectApplicationLifecycle } from '@agent-device/contracts/application-lifecycle-interaction'; const supported = Object.freeze({ available: true } as const); const linuxOwner = localRuntimeOwner('linux'); @@ -140,7 +140,9 @@ export function createLinuxPlatformRuntime(host: PlatformRuntimeHost): PlatformR throw new AppError('UNSUPPORTED_PLATFORM', 'Linux runtime cannot bind this device'); } const facts = linuxFacts(request.device); - const lifecycle = bindLinuxApplicationLifecycle({ + const lifecycle = bindLocalDirectApplicationLifecycle({ + owner: 'Linux', + openTargetIdentity: 'app-name', host: host.localInteractors, device: request.device, signal: request.scope.signal, diff --git a/packages/platform-vega/src/lifecycle.ts b/packages/platform-vega/src/lifecycle.ts deleted file mode 100644 index db7d5cf38b..0000000000 --- a/packages/platform-vega/src/lifecycle.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { - ApplicationLifecycleRuntimeOperations, - LocalApplicationInteractorHost, -} from '@agent-device/contracts/application-lifecycle-runtime'; -import { - bindDirectApplicationLifecycle, - bindLocalApplicationLifecycleInteractor, -} from '@agent-device/contracts/application-lifecycle-interaction'; -import type { DeviceInfo } from '@agent-device/kernel/device'; - -type VegaLifecycleParams = Readonly<{ - host: LocalApplicationInteractorHost; - device: DeviceInfo; - signal: AbortSignal; -}>; - -/** Vega owns its virtual-device lifecycle behavior after the local owner is bound. */ -export function bindVegaApplicationLifecycle( - params: VegaLifecycleParams, -): ApplicationLifecycleRuntimeOperations { - return bindDirectApplicationLifecycle({ - owner: 'Vega', - openTargetIdentity: 'app-name', - binding: bindLocalApplicationLifecycleInteractor({ - device: params.device, - signal: params.signal, - resolveInteractor: params.host.resolve, - }), - }); -} diff --git a/packages/platform-vega/src/runtime.ts b/packages/platform-vega/src/runtime.ts index 829bf35bd8..5f8274091b 100644 --- a/packages/platform-vega/src/runtime.ts +++ b/packages/platform-vega/src/runtime.ts @@ -22,7 +22,7 @@ import { gestureRuntimeOperationFacts } from '@agent-device/contracts/gesture-ru import { tvRemoteRuntimeOperationFacts } from '@agent-device/contracts/tv-remote-runtime'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; -import { bindVegaApplicationLifecycle } from './lifecycle.ts'; +import { bindLocalDirectApplicationLifecycle } from '@agent-device/contracts/application-lifecycle-interaction'; const vegaOwner = localRuntimeOwner('vega'); const lifecycleAvailable = Object.freeze({ available: true } as const); @@ -63,7 +63,9 @@ export function createVegaPlatformRuntime(host: PlatformRuntimeHost): PlatformRu throw new AppError('UNSUPPORTED_PLATFORM', 'Vega runtime cannot bind this device'); } const facts = vegaFacts(request.device); - const lifecycle = bindVegaApplicationLifecycle({ + const lifecycle = bindLocalDirectApplicationLifecycle({ + owner: 'Vega', + openTargetIdentity: 'app-name', host: host.localInteractors, device: request.device, signal: request.scope.signal, diff --git a/packages/platform-web/src/lifecycle.ts b/packages/platform-web/src/lifecycle.ts deleted file mode 100644 index bab8728b01..0000000000 --- a/packages/platform-web/src/lifecycle.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { - ApplicationLifecycleRuntimeOperations, - LocalApplicationInteractorHost, -} from '@agent-device/contracts/application-lifecycle-runtime'; -import { - bindDirectApplicationLifecycle, - bindLocalApplicationLifecycleInteractor, -} from '@agent-device/contracts/application-lifecycle-interaction'; -import type { DeviceInfo } from '@agent-device/kernel/device'; - -type WebLifecycleParams = Readonly<{ - host: LocalApplicationInteractorHost; - device: DeviceInfo; - signal: AbortSignal; -}>; - -/** Web owns browser lifecycle dispatch after its local owner is bound. */ -export function bindWebApplicationLifecycle( - params: WebLifecycleParams, -): ApplicationLifecycleRuntimeOperations { - return bindDirectApplicationLifecycle({ - owner: 'web', - openTargetIdentity: 'app-name', - binding: bindLocalApplicationLifecycleInteractor({ - device: params.device, - signal: params.signal, - resolveInteractor: params.host.resolve, - }), - }); -} diff --git a/packages/platform-web/src/runtime.ts b/packages/platform-web/src/runtime.ts index ba6982bf2a..aafbcdde49 100644 --- a/packages/platform-web/src/runtime.ts +++ b/packages/platform-web/src/runtime.ts @@ -63,7 +63,7 @@ import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import type { Interactor } from '@agent-device/contracts/interactor-types'; import { bindWebScreenRecordingRuntime } from './recording/runtime.ts'; -import { bindWebApplicationLifecycle } from './lifecycle.ts'; +import { bindLocalDirectApplicationLifecycle } from '@agent-device/contracts/application-lifecycle-interaction'; const owner = localRuntimeOwner('web'); const available = Object.freeze({ available: true } as const); @@ -308,7 +308,13 @@ function bindWebRuntime( }, })), ...availableApplicationLifecycleOperations( - bindWebApplicationLifecycle({ host: host.localInteractors, device, signal }), + bindLocalDirectApplicationLifecycle({ + owner: 'web', + openTargetIdentity: 'app-name', + host: host.localInteractors, + device, + signal, + }), facts.operations, ), }; From 9ee71769133d61be80545dc1692f2fbc68013233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 7 Oct 2026 22:15:45 +0200 Subject: [PATCH 3/4] refactor(harmonyos): bind the shared local interactor operation set Co-Authored-By: Claude Fable 5.1 --- packages/platform-harmonyos/src/runtime.ts | 80 +++------------------- 1 file changed, 10 insertions(+), 70 deletions(-) diff --git a/packages/platform-harmonyos/src/runtime.ts b/packages/platform-harmonyos/src/runtime.ts index 35e70c593e..2e08355b75 100644 --- a/packages/platform-harmonyos/src/runtime.ts +++ b/packages/platform-harmonyos/src/runtime.ts @@ -10,20 +10,11 @@ import { } from '@agent-device/contracts/application-lifecycle-runtime'; import { backRuntimeOperationFacts } from '@agent-device/contracts/back-runtime'; import { elementTextRuntimeOperationFacts } from '@agent-device/contracts/element-text-runtime'; -import { - bindLocalFocusInteractor, - focusRuntimeOperationFacts, -} from '@agent-device/contracts/focus-runtime'; +import { focusRuntimeOperationFacts } from '@agent-device/contracts/focus-runtime'; import { TARGET_AUTHORED_DRAG_UNSUPPORTED_HINT } from '@agent-device/contracts/gesture-admission'; -import { - bindLocalGestureInteractor, - gestureRuntimeOperationFacts, -} from '@agent-device/contracts/gesture-runtime'; -import { - bindLocalScrollInteractor, - scrollRuntimeOperationFacts, -} from '@agent-device/contracts/scroll-runtime'; -import { bindAdmittedLocalInteractorOperations } from '@agent-device/contracts/interactor-operation-catalog'; +import { gestureRuntimeOperationFacts } from '@agent-device/contracts/gesture-runtime'; +import { scrollRuntimeOperationFacts } from '@agent-device/contracts/scroll-runtime'; +import { bindLocalInteractorOperationSet } from '@agent-device/contracts/local-interactor-operation-set'; import { alertRuntimeOperationFacts } from '@agent-device/contracts/alert-runtime'; import { appEventRuntimeOperationFacts } from '@agent-device/contracts/app-event-runtime'; import { settingsRuntimeOperationFacts } from '@agent-device/contracts/settings-runtime'; @@ -38,24 +29,15 @@ import { unavailableFact, whenAdmitted, } from '@agent-device/contracts/platform-runtime'; -import { - bindLocalScreenshotInteractor, - screenshotRuntimeOperationFacts, -} from '@agent-device/contracts/screenshot-runtime'; +import { screenshotRuntimeOperationFacts } from '@agent-device/contracts/screenshot-runtime'; import { selectorObservationRuntimeOperationFacts } from '@agent-device/contracts/selector-observation-runtime'; import { bindLocalSnapshotInteractor, snapshotRuntimeOperationFacts, } from '@agent-device/contracts/snapshot-runtime'; import { tvRemoteRuntimeOperationFacts } from '@agent-device/contracts/tv-remote-runtime'; -import { - bindLocalTypeTextInteractor, - typeTextRuntimeOperationFacts, -} from '@agent-device/contracts/type-text-runtime'; -import { - bindLocalTouchInteractor, - touchRuntimeOperationFacts, -} from '@agent-device/contracts/touch-runtime'; +import { typeTextRuntimeOperationFacts } from '@agent-device/contracts/type-text-runtime'; +import { touchRuntimeOperationFacts } from '@agent-device/contracts/touch-runtime'; import { viewportRuntimeOperationFacts } from '@agent-device/contracts/viewport-runtime'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { createHarmonyAppLogRuntime } from './logs/runtime.ts'; @@ -347,45 +329,13 @@ export function createHarmonyPlatformRuntime(host: PlatformRuntimeHost): Platfor resolveInteractor: host.localInteractors.resolve, }) : {}), - ...(facts.operations.captureScreenshot.available - ? bindLocalScreenshotInteractor({ - device: request.device, - signal: request.scope.signal, - resolveInteractor: host.localInteractors.resolve, - }) - : {}), - ...(facts.operations.focusPoint.available - ? bindLocalFocusInteractor({ - device: request.device, - signal: request.scope.signal, - resolveInteractor: host.localInteractors.resolve, - }) - : {}), - ...(facts.operations.typeText.available - ? bindLocalTypeTextInteractor({ - device: request.device, - signal: request.scope.signal, - resolveInteractor: host.localInteractors.resolve, - }) - : {}), - ...bindLocalGestureInteractor({ - device: request.device, - signal: request.scope.signal, - facts: facts.operations, - resolveInteractor: host.localInteractors.resolve, - }), - ...(facts.operations.scrollDirection.available - ? bindLocalScrollInteractor({ - device: request.device, - signal: request.scope.signal, - resolveInteractor: host.localInteractors.resolve, - }) - : {}), - ...bindAdmittedLocalInteractorOperations({ + ...bindLocalInteractorOperationSet({ device: request.device, signal: request.scope.signal, resolveInteractor: host.localInteractors.resolve, facts: facts.operations, + pause: async (milliseconds) => + await host.clock.sleep(milliseconds, request.scope.signal), }), ...whenAdmitted(facts.operations.perfFrames, () => createHarmonyPerfOperations({ @@ -393,16 +343,6 @@ export function createHarmonyPlatformRuntime(host: PlatformRuntimeHost): Platfor device: request.device, }), ), - ...whenAdmitted(facts.operations.tapPoint, () => - bindLocalTouchInteractor({ - device: request.device, - signal: request.scope.signal, - resolveInteractor: host.localInteractors.resolve, - facts: facts.operations, - pause: async (milliseconds) => - await host.clock.sleep(milliseconds, request.scope.signal), - }), - ), listApps: async (input: { device: DeviceInfo; filter: 'all' | 'user-installed' }) => { request.scope.signal.throwIfAborted(); const { listHarmonyApps } = await import('./app-lifecycle.ts'); From fe7752045b0ec09b67c6efa228926b7548eec402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 8 Oct 2026 10:16:38 +0200 Subject: [PATCH 4/4] refactor(platforms): name HarmonyOS in the shared set's doc and return void from the uninstall step Co-Authored-By: Claude Fable 5.1 --- packages/contracts/src/local-interactor-operation-set.ts | 8 ++++---- packages/platform-apple/src/runner/runner-session.ts | 6 ++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/contracts/src/local-interactor-operation-set.ts b/packages/contracts/src/local-interactor-operation-set.ts index 9d624783e0..7e49d48a6f 100644 --- a/packages/contracts/src/local-interactor-operation-set.ts +++ b/packages/contracts/src/local-interactor-operation-set.ts @@ -14,10 +14,10 @@ import { bindLocalTypeTextInteractor } from './type-text-runtime.ts'; /** * The whole local interactor-backed operation set a pointer-driving family binds: the uniform * one-fact-one-bind leaves the interactor catalog walks, plus the four that carry extra input of their own — gesture and touch read the - * fact map, and touch needs a pause clock. Android and Linux each held a byte-identical copy of - * this list; it is one list now, so neither family can drift from the other by forgetting a - * member. Capture stays out: an owner's snapshot mechanics are its own (Linux captures a surface - * through its host, Android drives the interactor), so there is no shared reading to make. + * fact map, and touch needs a pause clock. Android, Linux, and HarmonyOS bind this one list, so + * no pointer-driving family can drift from another by forgetting a member. Capture stays out: an + * owner's snapshot mechanics are its own (Linux captures a surface through its host, Android + * drives the interactor), so there is no shared reading to make. */ export function bindLocalInteractorOperationSet( params: Readonly<{ diff --git a/packages/platform-apple/src/runner/runner-session.ts b/packages/platform-apple/src/runner/runner-session.ts index e087337db9..1200c5dc9f 100644 --- a/packages/platform-apple/src/runner/runner-session.ts +++ b/packages/platform-apple/src/runner/runner-session.ts @@ -7,7 +7,6 @@ import { buildSimctlArgsForDevice, runXcrun, } from './host.ts'; -import type { ExecResult } from '@agent-device/host-kit/command'; import { isApplePlatform, type DeviceInfo } from '@agent-device/kernel/device'; import type { RunnerLogicalLeaseContext } from '@agent-device/contracts/runner-lease-context'; import type { AppleRunnerLifecycleOptions } from './runner-provider.ts'; @@ -530,9 +529,9 @@ async function cleanupStaleSimulatorRunnerBundles(device: DeviceInfo): Promise { +): Promise { try { - return await runXcrun(buildSimctlArgsForDevice(device, ['uninstall', device.id, bundleId]), { + await runXcrun(buildSimctlArgsForDevice(device, ['uninstall', device.id, bundleId]), { allowFailure: true, timeoutMs: RUNNER_STALE_BUNDLE_UNINSTALL_TIMEOUT_MS, }); @@ -547,7 +546,6 @@ async function uninstallStaleSimulatorRunnerBundle( error: error instanceof Error ? error.message : String(error), }, }); - return undefined; } }