Why
The scenarios an agent most wants to test on its own start from a cold process: onboarding, a push notification or deep link opening the app from scratch, a data migration, "does the cart survive a kill". By design, process death ends the session (resume tokens never touch disk, docs/ARCHITECTURE.md §1, §3, §6), and this issue does not propose changing that.
appduct link --open <target> already does most of a restart: delivering a link to a running app supersedes the session it holds (AppductClient+Session.swift's connect(input, supersede: holdsSession) and its Kotlin/JS counterparts), and delivering to a dead app launches it (simctl openurl, am start). What it does not do:
- Kill the process first.
--relaunch exists only for the experimental ios-device target, as devicectl --terminate-existing. On ios-sim and android a delivered link is a warm deep link into the running app, never a cold start.
- Keep the alias. When the app closes its old socket (superseded) or dies (killed), the daemon suspends the old session for
graceSeconds (600 s by default) rather than ending it. The new claim from the same device model therefore slugs to <model>-2, the session id changes, and with several devices live every namespaced MCP tool name shifts. An agent that scripted pixel-8__seed_cart is now calling a tool that no longer exists.
- Do it from MCP.
appduct_connect has relaunch for ios-device only, and no way to end the superseded session.
Proposal
Two flags on the existing link command and the existing appduct_connect tool. No new command, no new RPC method, no daemon-side memory of where a link was delivered.
--relaunch for every --open target. Terminate before delivering:
| target |
terminate |
then |
ios-sim |
xcrun simctl terminate <udid> <bundleId> |
existing simctl openurl |
android |
adb -s <serial> shell am force-stop <appId> |
existing am start |
ios-device |
--terminate-existing (unchanged) |
unchanged |
Both new paths need the app id, which is what #64 makes explicit for android and ios-device; ios-sim takes the same --app-id/--bundle-id value, and iosBundleId in config.json covers it as it does for ios-device. "Terminate" of an app that is not running is a no-op, not an error, so the flag is safe to pass unconditionally in a script.
--replace [selector]. Revoke the named session (or, with no selector, the single live session; ambiguous_session if several) immediately before delivering, so its alias is freed and the new claim reuses it. sessions.revoke already exists; this only sequences it. Without --replace, today's behaviour is unchanged: the old session stays suspended for the grace window.
MCP. appduct_connect gains relaunch for all targets and replace?: string | true, mapped to the same code. The result already reports sessionId; add replacedSessionId when replace was used, and alias, so the agent can see that the names it knew are still valid.
Optional and Android only: --clear-data runs adb shell pm clear <appId> between terminate and deliver, for a true first-launch test. iOS simulators have no cheap equivalent short of uninstall, so leave it out rather than fake it.
Where it lives
- Terminate commands next to the launch commands in
packages/appduct/src/cli/open-target.ts, behind the existing injectable ExecFn seam, so they are unit-tested without a device.
--relaunch is already parsed for link; widen its target check and thread it into deliverToOpenTarget. --replace goes through packages/appduct/src/commands/link.ts (one sessions.revoke call before link.create), the route in cli/routes/link.ts, and the cli.command(...) spec in create-cli.ts.
packages/appduct/src/mcp/connect-tool.ts for the MCP side, sharing the same helpers.
- Skill: replace the implicit "the session survives reloads" with a short "Restart the app" note in the CLI and MCP flows:
appduct link --open ios-sim --relaunch --replace, or appduct_connect({ relaunch: true, replace: true }).
Acceptance criteria
Out of scope
- Persisting resume tokens so a session survives process death.
- Automatic detection of which device a session belongs to. The agent re-passes (or lets
appduct_connect auto-detect) the target exactly as it did the first time.
- Clearing app data on iOS simulators.
Why
The scenarios an agent most wants to test on its own start from a cold process: onboarding, a push notification or deep link opening the app from scratch, a data migration, "does the cart survive a kill". By design, process death ends the session (resume tokens never touch disk,
docs/ARCHITECTURE.md§1, §3, §6), and this issue does not propose changing that.appduct link --open <target>already does most of a restart: delivering a link to a running app supersedes the session it holds (AppductClient+Session.swift'sconnect(input, supersede: holdsSession)and its Kotlin/JS counterparts), and delivering to a dead app launches it (simctl openurl,am start). What it does not do:--relaunchexists only for the experimentalios-devicetarget, asdevicectl --terminate-existing. Onios-simandandroida delivered link is a warm deep link into the running app, never a cold start.graceSeconds(600 s by default) rather than ending it. The new claim from the same device model therefore slugs to<model>-2, the session id changes, and with several devices live every namespaced MCP tool name shifts. An agent that scriptedpixel-8__seed_cartis now calling a tool that no longer exists.appduct_connecthasrelaunchforios-deviceonly, and no way to end the superseded session.Proposal
Two flags on the existing
linkcommand and the existingappduct_connecttool. No new command, no new RPC method, no daemon-side memory of where a link was delivered.--relaunchfor every--opentarget. Terminate before delivering:ios-simxcrun simctl terminate <udid> <bundleId>simctl openurlandroidadb -s <serial> shell am force-stop <appId>am startios-device--terminate-existing(unchanged)Both new paths need the app id, which is what #64 makes explicit for
androidandios-device;ios-simtakes the same--app-id/--bundle-idvalue, andiosBundleIdinconfig.jsoncovers it as it does forios-device. "Terminate" of an app that is not running is a no-op, not an error, so the flag is safe to pass unconditionally in a script.--replace [selector]. Revoke the named session (or, with no selector, the single live session;ambiguous_sessionif several) immediately before delivering, so its alias is freed and the new claim reuses it.sessions.revokealready exists; this only sequences it. Without--replace, today's behaviour is unchanged: the old session stays suspended for the grace window.MCP.
appduct_connectgainsrelaunchfor all targets andreplace?: string | true, mapped to the same code. The result already reportssessionId; addreplacedSessionIdwhenreplacewas used, andalias, so the agent can see that the names it knew are still valid.Optional and Android only:
--clear-datarunsadb shell pm clear <appId>between terminate and deliver, for a true first-launch test. iOS simulators have no cheap equivalent short of uninstall, so leave it out rather than fake it.Where it lives
packages/appduct/src/cli/open-target.ts, behind the existing injectableExecFnseam, so they are unit-tested without a device.--relaunchis already parsed forlink; widen its target check and thread it intodeliverToOpenTarget.--replacegoes throughpackages/appduct/src/commands/link.ts(onesessions.revokecall beforelink.create), the route incli/routes/link.ts, and thecli.command(...)spec increate-cli.ts.packages/appduct/src/mcp/connect-tool.tsfor the MCP side, sharing the same helpers.appduct link --open ios-sim --relaunch --replace, orappduct_connect({ relaunch: true, replace: true }).Acceptance criteria
appduct link --open ios-sim --relaunch --replaceagainst a running app yields a newactivesession with the same alias in one command; same for--open android.appduct_connect({ relaunch: true, replace: true })followed byappduct_wait_for_sessiondoes the same over MCP, andnotifications/tools/list_changedfires with the proxied names unchanged in a single-device setup.--relaunchwithout an app id onios-sim/androidis a usage error naming the flag and the config key, before anything is minted or terminated.--replacewith several live sessions and no selector fails withambiguous_sessionlisting the aliases, before anything is minted or terminated.ExecFnfor all three targets, including the "app not running" no-op; an integration test with a fake app covers revoke-then-claim alias stability.appductREADME command table,docs/ARCHITECTURE.md§8,skills/appduct/SKILL.mdandCHANGELOG.mdupdated.Out of scope
appduct_connectauto-detect) the target exactly as it did the first time.