Repository navigation
feat(maestro): support killApp via close mode - #2749
Conversation
|
Reviewed 78727e3. I found no code defect, but the kill route has only run against adb fakes, and I have one smaller-design question. The route (Maestro replay → close with Could this be smaller? Only the Android lifecycle owner reads Not blocking: the support matrix and replay-e2e.md do not say that a foreground killApp is refused rather than a silent no-op; a process that survives All checks are green and there are no conflicts. I could not check whether upstream Maestro's killApp also refuses a foreground app. |
|
@Rohit3523 would you like to followup or you're good with me overtaking this? |
|
Hey! Yeah, I’m going to check the review and push the changes. I’m just waiting for the weekend 😄 It would be great to collaborate and get this thing done together ^_^ |
There was a problem hiding this comment.
7 issues found across 27 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/platform-android/src/__tests__/app-lifecycle-open.test.ts">
<violation number="1" location="packages/platform-android/src/__tests__/app-lifecycle-open.test.ts:239">
P3: These two tests invoke `killAndroidApp` twice because `assertRejectsAppError` (test-utils/app-error.ts) only matches `code`/`message`/`hint`, so a second raw call is needed just to read `details.reason`. In the survivor test the second invocation re-runs the whole failure path, adding another ~2 s of real time (ANDROID_CLOSE_PROCESS_TIMEOUT_MS poll loop) to CI and duplicating identical adb mock traffic. Either extend `ExpectedAppError` with a `details` partial matcher and assert `code`+`hint`+`reason` in one `assertRejectsAppError`, or replace the double call with a single try/catch that asserts code, hint, and `details.reason` together.</violation>
</file>
<file name="src/daemon/session-lifecycle/internal/__tests__/session-close-lifecycle-runtime.test.ts">
<violation number="1" location="src/daemon/session-lifecycle/internal/__tests__/session-close-lifecycle-runtime.test.ts:338">
P3: `mockBindDeviceRuntime.mockImplementation(...)` permanently replaces the shared harness mock for the rest of this file: the file's `beforeEach` only calls `mockClear`, which keeps implementations, so the wrapper (and its `seenModes` closure) stays installed for every subsequent test here. Such tests still pass only because the wrapper delegates to `baseBind`, but the shared mock is left hijacked, which silently couples this test to all later tests in the file. Restore the base implementation at the end of the test, or use two `mockImplementationOnce` registrations.</violation>
</file>
<file name="src/daemon/session-lifecycle/internal/session-close.ts">
<violation number="1" location="src/daemon/session-lifecycle/internal/session-close.ts:152">
P1: This only propagates `kill` through the session-backed dispatcher. A Maestro flow that starts before a daemon session exists enters `closeWithoutSession`, where the omitted mode makes Android perform `stop` instead of `killApp`; pass the same mode through that branch too.</violation>
</file>
<file name="packages/platform-android/src/app-lifecycle.ts">
<violation number="1" location="packages/platform-android/src/app-lifecycle.ts:512">
P2: `killAndroidPackage` proceeds with `am kill` when the foreground probe cannot produce a state. `getAndroidAppState` converts failed or empty dumps into `{}`, so this guard cannot enforce the documented foreground precondition; fail closed or surface the probe failure before dispatching.</violation>
<violation number="2" location="packages/platform-android/src/app-lifecycle.ts:521">
P1: This final liveness check cannot prove the process is gone because `isAndroidPackageProcessRunning` treats every empty `pidof` result as false, including probe failures. Distinguish the normal no-process exit from command errors and fail when the liveness probe itself is unavailable.</violation>
</file>
<file name="packages/maestro/src/internal/__tests__/runtime-port.test.ts">
<violation number="1" location="packages/maestro/src/internal/__tests__/runtime-port.test.ts:187">
P3: The explicit and config app ids are the same value, so this test cannot distinguish the two paths it claims to cover: the dispatcher computes `{ appId: command.appId ?? request.appId }`, and both commands resolve to `com.example.checkout` regardless of which branch is exercised. A regression that, for example, always used the config app id and dropped the explicit `killApp:` app id would still pass. Use a distinct explicit app id (e.g. `com.example.other`) and assert calls[0] receives it while calls[1] falls back to the config id.</violation>
</file>
<file name="packages/platform-android/src/lifecycle.ts">
<violation number="1" location="packages/platform-android/src/lifecycle.ts:69">
P2: `mode: 'kill'` with an empty `positionals` returns success without killing anything. Fall back to `input.appBundleId` (the session-app identity this binding uses everywhere else) and throw `INVALID_ARGS` when neither exists, so a kill that has no target fails loudly instead of reading as success.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| @@ -149,6 +149,7 @@ async function dispatchTargetedPlatformClose(params: { | |||
| outPath: req.flags?.out, | |||
| appBundleId: session.appBundleId, | |||
| surface: session.surface ?? 'app', | |||
| ...(req.internal?.killApp === true ? { mode: 'kill' as const } : {}), | |||
There was a problem hiding this comment.
P1: This only propagates kill through the session-backed dispatcher. A Maestro flow that starts before a daemon session exists enters closeWithoutSession, where the omitted mode makes Android perform stop instead of killApp; pass the same mode through that branch too.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/daemon/session-lifecycle/internal/session-close.ts, line 152:
<comment>This only propagates `kill` through the session-backed dispatcher. A Maestro flow that starts before a daemon session exists enters `closeWithoutSession`, where the omitted mode makes Android perform `stop` instead of `killApp`; pass the same mode through that branch too.</comment>
<file context>
@@ -149,6 +149,7 @@ async function dispatchTargetedPlatformClose(params: {
outPath: req.flags?.out,
appBundleId: session.appBundleId,
surface: session.surface ?? 'app',
+ ...(req.internal?.killApp === true ? { mode: 'kill' as const } : {}),
execution: applicationLifecycleExecutionFromRequest(req, logPath, session.trace?.outPath),
});
</file context>
| } | ||
| await runAndroidShell(device, ['am', 'kill', packageName]); | ||
| await waitForAndroidPackageStopped(device, packageName); | ||
| if (await isAndroidPackageProcessRunning(device, packageName)) { |
There was a problem hiding this comment.
P1: This final liveness check cannot prove the process is gone because isAndroidPackageProcessRunning treats every empty pidof result as false, including probe failures. Distinguish the normal no-process exit from command errors and fail when the liveness probe itself is unavailable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/platform-android/src/app-lifecycle.ts, line 521:
<comment>This final liveness check cannot prove the process is gone because `isAndroidPackageProcessRunning` treats every empty `pidof` result as false, including probe failures. Distinguish the normal no-process exit from command errors and fail when the liveness probe itself is unavailable.</comment>
<file context>
@@ -488,6 +488,44 @@ export async function closeAndroidApp(device: DeviceInfo, app: string): Promise<
+ }
+ await runAndroidShell(device, ['am', 'kill', packageName]);
+ await waitForAndroidPackageStopped(device, packageName);
+ if (await isAndroidPackageProcessRunning(device, packageName)) {
+ throw new AppError('COMMAND_FAILED', `am kill did not stop ${packageName}`, {
+ reason: 'android-kill-process-survived',
</file context>
| } | ||
|
|
||
| async function killAndroidPackage(device: DeviceInfo, packageName: string): Promise<void> { | ||
| const foreground = await readAndroidForegroundApp(device); |
There was a problem hiding this comment.
P2: killAndroidPackage proceeds with am kill when the foreground probe cannot produce a state. getAndroidAppState converts failed or empty dumps into {}, so this guard cannot enforce the documented foreground precondition; fail closed or surface the probe failure before dispatching.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/platform-android/src/app-lifecycle.ts, line 512:
<comment>`killAndroidPackage` proceeds with `am kill` when the foreground probe cannot produce a state. `getAndroidAppState` converts failed or empty dumps into `{}`, so this guard cannot enforce the documented foreground precondition; fail closed or surface the probe failure before dispatching.</comment>
<file context>
@@ -488,6 +488,44 @@ export async function closeAndroidApp(device: DeviceInfo, app: string): Promise<
+}
+
+async function killAndroidPackage(device: DeviceInfo, packageName: string): Promise<void> {
+ const foreground = await readAndroidForegroundApp(device);
+ if (foreground?.package === packageName) {
+ throw new AppError('COMMAND_FAILED', `Cannot kill foreground app ${packageName}`, {
</file context>
| } | ||
| if (input.mode === 'kill') { | ||
| const target = input.positionals[0]; | ||
| if (target) await killAndroidApp(device, target); |
There was a problem hiding this comment.
P2: mode: 'kill' with an empty positionals returns success without killing anything. Fall back to input.appBundleId (the session-app identity this binding uses everywhere else) and throw INVALID_ARGS when neither exists, so a kill that has no target fails loudly instead of reading as success.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/platform-android/src/lifecycle.ts, line 69:
<comment>`mode: 'kill'` with an empty `positionals` returns success without killing anything. Fall back to `input.appBundleId` (the session-app identity this binding uses everywhere else) and throw `INVALID_ARGS` when neither exists, so a kill that has no target fails loudly instead of reading as success.</comment>
<file context>
@@ -63,6 +64,11 @@ export function bindAndroidApplicationLifecycle(
}
+ if (input.mode === 'kill') {
+ const target = input.positionals[0];
+ if (target) await killAndroidApp(device, target);
+ return;
+ }
</file context>
| }, | ||
| { serial: 'emulator-5554' }, | ||
| async () => { | ||
| await assertRejectsAppError(() => killAndroidApp(device, 'com.example.app'), { |
There was a problem hiding this comment.
P3: These two tests invoke killAndroidApp twice because assertRejectsAppError (test-utils/app-error.ts) only matches code/message/hint, so a second raw call is needed just to read details.reason. In the survivor test the second invocation re-runs the whole failure path, adding another ~2 s of real time (ANDROID_CLOSE_PROCESS_TIMEOUT_MS poll loop) to CI and duplicating identical adb mock traffic. Either extend ExpectedAppError with a details partial matcher and assert code+hint+reason in one assertRejectsAppError, or replace the double call with a single try/catch that asserts code, hint, and details.reason together.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/platform-android/src/__tests__/app-lifecycle-open.test.ts, line 239:
<comment>These two tests invoke `killAndroidApp` twice because `assertRejectsAppError` (test-utils/app-error.ts) only matches `code`/`message`/`hint`, so a second raw call is needed just to read `details.reason`. In the survivor test the second invocation re-runs the whole failure path, adding another ~2 s of real time (ANDROID_CLOSE_PROCESS_TIMEOUT_MS poll loop) to CI and duplicating identical adb mock traffic. Either extend `ExpectedAppError` with a `details` partial matcher and assert `code`+`hint`+`reason` in one `assertRejectsAppError`, or replace the double call with a single try/catch that asserts code, hint, and `details.reason` together.</comment>
<file context>
@@ -162,6 +162,151 @@ test('closeAndroidApp waits until package process exits after force-stop', async
+ },
+ { serial: 'emulator-5554' },
+ async () => {
+ await assertRejectsAppError(() => killAndroidApp(device, 'com.example.app'), {
+ code: 'COMMAND_FAILED',
+ hint: /Background the app before killApp/,
</file context>
| sessionStore.set(sessionName, session); | ||
| const seenModes: Array<unknown> = []; | ||
| const baseBind = mockBindDeviceRuntime.getMockImplementation(); | ||
| mockBindDeviceRuntime.mockImplementation(async (boundDevice, use) => { |
There was a problem hiding this comment.
P3: mockBindDeviceRuntime.mockImplementation(...) permanently replaces the shared harness mock for the rest of this file: the file's beforeEach only calls mockClear, which keeps implementations, so the wrapper (and its seenModes closure) stays installed for every subsequent test here. Such tests still pass only because the wrapper delegates to baseBind, but the shared mock is left hijacked, which silently couples this test to all later tests in the file. Restore the base implementation at the end of the test, or use two mockImplementationOnce registrations.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/daemon/session-lifecycle/internal/__tests__/session-close-lifecycle-runtime.test.ts, line 338:
<comment>`mockBindDeviceRuntime.mockImplementation(...)` permanently replaces the shared harness mock for the rest of this file: the file's `beforeEach` only calls `mockClear`, which keeps implementations, so the wrapper (and its `seenModes` closure) stays installed for every subsequent test here. Such tests still pass only because the wrapper delegates to `baseBind`, but the shared mock is left hijacked, which silently couples this test to all later tests in the file. Restore the base implementation at the end of the test, or use two `mockImplementationOnce` registrations.</comment>
<file context>
@@ -319,6 +319,56 @@ test('a supported Android close clears admitted runtime hints exactly once', asy
+ sessionStore.set(sessionName, session);
+ const seenModes: Array<unknown> = [];
+ const baseBind = mockBindDeviceRuntime.getMockImplementation();
+ mockBindDeviceRuntime.mockImplementation(async (boundDevice, use) => {
+ const binding = await baseBind!(boundDevice, use);
+ const innerClose = binding.operations.closeApplication;
</file context>
| killApp: vi.fn(async (input, context) => record(calls, 'killApp', input, context)), | ||
| }); | ||
| const program = parseMaestroProgram( | ||
| ['appId: com.example.checkout', '---', '- killApp: com.example.checkout', '- killApp'].join( |
There was a problem hiding this comment.
P3: The explicit and config app ids are the same value, so this test cannot distinguish the two paths it claims to cover: the dispatcher computes { appId: command.appId ?? request.appId }, and both commands resolve to com.example.checkout regardless of which branch is exercised. A regression that, for example, always used the config app id and dropped the explicit killApp: app id would still pass. Use a distinct explicit app id (e.g. com.example.other) and assert calls[0] receives it while calls[1] falls back to the config id.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/maestro/src/internal/__tests__/runtime-port.test.ts, line 187:
<comment>The explicit and config app ids are the same value, so this test cannot distinguish the two paths it claims to cover: the dispatcher computes `{ appId: command.appId ?? request.appId }`, and both commands resolve to `com.example.checkout` regardless of which branch is exercised. A regression that, for example, always used the config app id and dropped the explicit `killApp:` app id would still pass. Use a distinct explicit app id (e.g. `com.example.other`) and assert calls[0] receives it while calls[1] falls back to the config id.</comment>
<file context>
@@ -178,6 +178,25 @@ describe('MaestroRuntimePort', () => {
+ killApp: vi.fn(async (input, context) => record(calls, 'killApp', input, context)),
+ });
+ const program = parseMaestroProgram(
+ ['appId: com.example.checkout', '---', '- killApp: com.example.checkout', '- killApp'].join(
+ '\n',
+ ),
</file context>
|
The Compatibility & Provenance → Run Fallow audit failure is pre-existing mainline drift, not introduced by this PR. I reproduced the audit locally at this PR's head (
The 4 complexity hits ( Suggested path: the owning mainline changes address those three findings (or get path-keyed baseline updates), then a rebase turns this job green. Per repo rules I'm keeping this PR within its command-family scope and not touching those files or regenerating baselines. |
|
Following up on f894395 after the earlier evidence-pending review at 78727e3. The kill route (packages/platform-android/src/app-lifecycle.ts#L510) is still only exercised through adb exec fakes: Maestro replay calls close with closeAppOnly+killApp, lifecycle.ts:70 calls killAndroidPackage, which does a foreground probe, am kill, waitForAndroidPackageStopped, then a pidof check. Neither this delta nor any author comment adds the emulator replay the earlier review asked for. Nobody has shown that am kill actually reaps an app that was sent to background with Home on a real emulator, or that the pidof check correctly reads a backgrounded-but-alive process. Either gap could make killApp fail with android-kill-process-survived, or report success without the process being killed. Can you run Not blocking, can be taken or left: the stricter isAndroidPackageProcessRunning (app-lifecycle.ts#L585) now throws android-process-probe-unavailable on any pidof hiccup, which would abort close/stopApp/relaunch paths on a single flaky probe even after force-stop already succeeded, so should a probe failure only fail the operation whose contract needs proof of death, and should closeWithoutSession's mode spread in session-close.ts#L439 get a direct test (closing with {closeAppOnly, killApp} and no stored session, asserting mode 'kill' reaches platform closeApplication) since nothing exercises it today; also the killAndroidApp re-export in mechanics.ts#L86 looks like dead leftover code with no consumer at head, and the PR body still references the old Interactor.kill design and a different tested commit, so worth cleaning up before merge. Compatibility & Provenance ran fallow against the pre-rebase base and its findings sit in files this PR does not touch (session-test-harness.ts, runner-contract.ts, runner/index.ts, the apple/harmonyos recovery.ts pair, run-script-http.ts, install-source-network-transport.ts, args.ts); the one file this PR does touch, conformance-normalize.ts, only gets new cases in the lifecycle sub-helpers, not in the flagged canonicalizeUpstreamCommand or canonicalizeAgentCommand, so this failure looks unrelated to the change. I did not re-run the author's reproduction on main, so I can't confirm main actually reproduces the failure being fixed here. I also did not run the unit suites; the regression read above comes from reading the diff and the pre-change code, not from a run. I can't say which Android images in the fleet lack pidof or emit stderr noise from it, so the probe-hardening impact is plausible, not confirmed. And I don't know whether upstream Maestro's killApp also refuses a foreground app. The remaining requirement before this can merge is the live Android emulator replay described above: killApp after Home must cold-kill the app (empty pidof, cold relaunch), and killApp on a foreground app must fail with android-kill-requires-background-app. |
|
Ran the requested live Android emulator replay ( What the live run foundFlow: The refusal didn't fire. With a temporary debug probe in
Fix ( Caveat: couldn't get a clean live confirmation of the background-kill + cold-relaunch caseThis dev machine had several concurrent iOS simulators + an The foreground-refusal fix itself doesn't depend on that confound — it's proven by the debug-instrumented live read above (a value, not a pass/fail) plus the new deterministic unit test. The three non-blocking cleanups
|
There was a problem hiding this comment.
All reported issues were addressed across 4 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
|
This is a follow-up on f894395 (#2749 (comment)) at e676ade, and the fixes there don't close the gap. readAndroidForegroundAppSettled (packages/platform-android/src/app-lifecycle.ts#L579) still re-reads the same source that went stale in the live run, just 150 ms later. getAndroidAppState ranks WMS mCurrentFocus ahead of the AMS resumed activity (window-state.ts ANDROID_WINDOW_QUESTIONS.foreground), and three inputs still take the wrong branch on that ranking. If the focus lag after launchApp outlasts 150 ms, which a heavy RN cold start can do, the refusal is skipped, Not blocking: the new JSDoc and the test headers at app-lifecycle.ts#L565, app-lifecycle-kill.test.ts:53, and session-close-without-session-mode.test.ts:44 carry dated live-run narrative and a "#2749 review:" history that AGENTS.md asks to keep out of implementation comments, worth removing once the fix above lands but not a reason to hold this up on its own. The iOS runner XCTest testSynthesizedReplacementPacesAnAppOwnedFieldAtItsAcknowledgeWindow (RunnerTests+SynthesizedTextEntryTests.swift:120) is failing in CI, but this PR touches no Swift or runner file, its route is Maestro → daemon close → Android lifecycle, and main recently changed that exact fixture in 94433db, so that failure looks unrelated to this change. The Fallow findings are against a stale base (a798792, 918 changed files); every flagged symbol, clone, and complexity hit sits in a file this PR doesn't touch, or is a pre-existing export (android test-utils/app-error.ts assertThrowsAppError) whose line only moved, identical on main, so a rebase should clear those. There are no known conflicts. I didn't run the unit suites; the test-validity read above comes from reading the pre-delta and head code, not from executing it. I also don't know how long the WMS focus lag actually runs after launchApp or after Home on a real device, so the launch-lag and Home-lag cases in the finding above are marked likely rather than confirmed, and I haven't checked whether upstream Maestro's own killApp refuses or auto-backgrounds a foreground app. The author's background-kill replay was confounded by the OOM killer by their own account, so Before this can merge, the kill precondition needs to switch to the AMS resumed activity as above, and then needs a live emulator replay, on an emulator with no concurrent simulators or xcodebuild and no lowmemorykiller/signal 9 for the package in logcat: |
|
Reviewed at e3387ef. The precondition still comes from the wrong source, and there's no live proof the shipped behavior actually kills a backgrounded app.
The kill precondition that actually touches the device changed twice, at e676ade and again here. The only live run so far was on older code with a debug probe, and its background-kill result was confounded by Once the resumed-activity fix above lands, this needs a live replay on the shipped commit, on an emulator with no concurrent simulators or xcodebuild running and no I didn't run the unit suites; the read above is from tracing the code before and after the delta, not from execution. I also don't know how long WMS focus lag runs on real devices after The precondition needs to move off WMS focus onto the AMS resumed activity through a window-state question, the sleep-and-reread helper needs to go, and then the live Android replay described above needs to run clean before this is ready to merge. |
There was a problem hiding this comment.
All reported issues were addressed across 4 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
|
This is a follow-up on the earlier review at e3387ef. The precondition changed again, from WMS focus to AMS resumed activity: https://github.com/callstack/agent-device/blob/23ea708/packages/platform-android/src/app-lifecycle.ts#L511. No live replay has run on 23ea708; the only prior run predates e676ade and was confounded by lowmemorykiller SIGKILLs. So there is still no proof that the shipped precondition allows a backgrounded kill, that am kill (not the OOM killer) reaps the process, that the next launch is cold, or that a foreground target is refused with the typed reason. Please run the replay from the earlier review on 23ea708 or later, with no concurrent simulators or xcodebuild running: launchApp, pressKey Home, killApp, launchApp (stopApp:false) must show killApp succeeding, an empty Not blocking: getAndroidResumedActivity returns I did not run the unit suites; the claim that tests fail on the old code comes from tracing the pre-delta code, not from a run. I also don't know whether real devices can show no resumed record between Home and killApp, so the note above stays a question rather than a confirmed finding. The delta adds |
Maestro killApp is system-initiated process death. On Android it dispatches am kill after reading the AMS resumed activity: a resumed target is refused with android-kill-requires-background-app, and a process that survives fails with android-kill-process-survived. Other platforms ignore the mode and keep an ordinary close. Co-authored-by: Rohit <40559587+Rohit3523@users.noreply.github.com>
23ea708 to
4661ed9
Compare
|
@Rohit3523 thanks for driving this. I rebased the branch onto current main as a single commit (you're the co-author) and closed out the remaining review items: the kill reads the AMS resumed activity, a dump that answers with nothing resumed now proceeds, the strict 🤖 Generated with Claude Code |
|
The code at 4661ed9 looks good, and the earlier evidence request from the review of 23ea708 is now met. Not blocking: parseAndroidResumedActivity (app-parsers.ts:105) repeats the line and marker loop from parseAndroidFocusSegment (app-parsers.ts:85) and adds a fourth inline copy of the package/activity regex, so could parseAndroidFocusSegment take a markers parameter and could one shared component parser serve all four sites? You can take or leave this. The live Android 37 replay (logcat "kill background", cold relaunch pid 6043, foreground refusal) is from your report in the PR body. I could not observe it and did not rerun it. I also did not check whether an Appium-backed Android route uses bindAndroidApplicationLifecycle. The docs name ADB-backed Android and say WebDriver-provider backends run killApp as stopApp, which matches the single mode read site. CI on 4661ed9 is still pending, with 9 jobs queued or in progress and no failures so far. These jobs cover the changed Android lifecycle, Maestro runtime port and daemon session-close routes. CI must go green before merge. There are no conflicts. The open bot threads no longer apply and can be resolved. All are fixed at head:
|
Summary
Supports Maestro
killApp, which kills the app process the way the system would. On ADB-backed Android it runsam kill. iOS, Limrun and WebDriver-provider backends treat it asstopApp, and the docs and support matrix say so. There is no new public CLI surface.android-kill-requires-background-app. A process that survives the kill fails withandroid-kill-process-survived. If no activity dump answers, the kill is refused withandroid-process-probe-unavailable. If the dump answers but shows nothing resumed, the kill goes ahead.pidofcheck applies only to the kill's final check. Ordinary close, stopApp and relaunch keep main's behaviour.upstream/116_kill_appis no longer listed as a divergence. The preconditions are recorded inDOCUMENTED_DEVIATIONS.Validation
Tested commit
4661ed90c5.pnpm check:affected --run: green (full fail-open set; 4856 unit tests).pnpm maestro:conformance: 62/62 pass.Live
agent-device replay --maestroon an Android 37 emulator with a fresh daemon and no other emulators or simulators running:Killing 5797:com.callstack.agentdevicelab/u0a231 (adj 700): kill backgroundright afteram kill.pidofprints nothing, and there are no lowmemorykiller or signal-9 lines.stopApp: false): cold start, new pid 6043 (Start proc 6043 … for next-top-activity).android-kill-requires-background-app.ResumedActivitystill namescom.callstack.agentdevicelab/.MainActivity, and the process survives.Unresolved: if
am killruns before the system has finished backgrounding the app (for example a picture-in-picture app), the process may survive and the kill fail. This is an inference; it did not happen in the live runs.🤖 Generated with Claude Code