You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
CommandTraits.readOnly is documented as one axis and consumed as several, so opting a command out of one decision silently opts it out of the others. querySelector is the live instance: it is deliberately not retried, and as a side effect it is no longer refused when its app is stopped, so it bare-launches the app instead of answering APP_NOT_RUNNING.
This issue separates app-launch policy from retry eligibility inside the existing exhaustive classification, and fixes the command's behavior.
Citations are at f47e9c9ef4.
Current behavior
RunnerTests+Models.swift:52-53 documents readOnly as "Whether the command is eligible for the session-invalidating retry". The struct comment at :44-48 presents the table as three independent axes.
Five runner-side decisions read that one flag: retry (RunnerTests+Lifecycle.swift:454), the stopped-app refusal notRunningReadResponse (:321-325), activation routing (RunnerTests+CommandDispatch.swift:445-451), recorded-failure→session invalidation (:536, jointly with isLifecycle), and remembered-text-entry tap clearing (RunnerTests+CommandExecution.swift:11).
querySelector is classified readOnly: .never (RunnerTests+Models.swift:116, comment at :114). Because the refusal and the activation both read the retry bit, a querySelector whose target app is .notRunning skips notRunningReadResponse and reaches activateTarget(bundleId:reason:"bundle_changed") at CommandDispatch.swift:451. fix(ios): never launch a not-running session app from an observation #2852 established that a runner read must not launch a stopped app.
TypeScript classifies the same command read-only (packages/platform-apple/src/runner/runner-command-traits.ts:65).
Required behavior
A read command never causes a bare launch. querySelector against a stopped app answers APP_NOT_RUNNING and stays non-retried.
Launch policy is its own declared fact — e.g. noApp | existingApp | mayLaunch — resolved in the same exhaustive CommandType.traits switch, and the not-running refusal lives under it. A change to retry eligibility must not be able to change it.
Payload-dependent traits resolve once against Command, removing CommandTraits.ReadOnly.conditional and the alert-specific resolver at RunnerTests+Lifecycle.swift:465-473. Conditionality is per fact: alertget permits read retry, accept/dismiss do not, and every alert action skips app-activation preflight (RunnerTests+CommandDispatch.swift:549-551).
The activation bypass and the recorded-failure exemption stop sharing isLifecycle. The comment at RunnerTests+Models.swift:100-104 exists only to explain that collision and should be deleted by this change.
Five named wrappers over one retry bit do not satisfy this issue. The coupling to break is that launch policy reads the retry bit.
Out of scope
Do not force the two trait tables equal. They are homonyms, not twins: TS readOnly also gates readiness preflight (runner-session.ts:1205), waitForRunner versus a single send (:1034-1045), and transport recovery (runner-command-recovery.ts:247,322). querySelector and uptime deliberately allow the TS resend while forbidding the Swift session-loss retry. Genuinely shared facts belong in the table that already exists for them (contracts/fixtures/alert-command-traits.json, read by both RunnerTests+LifecycleTests.swift and packages/platform-apple/src/runner/__tests__/runner-command-traits.test.ts); everything else stays per-side.
Completion conditions
One per-command table asserts every runner-side consumer together: retry, not-running refusal, activation route, recorded-failure conversion.
On a live simulator, querySelector against a stopped app returns APP_NOT_RUNNING, and a tap in the same state still activates.
ReadOnly.conditional and the Models.swift:100-104 comment are gone.
A new CommandType case cannot compile without classifying its launch policy.
pnpm check:affected --run and the runner XCTest lanes pass on the pushed commit.
Dependencies
Found by the #2803 audit. Related: #2881 (request shapes, not trait semantics), #2852, #2862.
Purpose
CommandTraits.readOnlyis documented as one axis and consumed as several, so opting a command out of one decision silently opts it out of the others.querySelectoris the live instance: it is deliberately not retried, and as a side effect it is no longer refused when its app is stopped, so it bare-launches the app instead of answeringAPP_NOT_RUNNING.This issue separates app-launch policy from retry eligibility inside the existing exhaustive classification, and fixes the command's behavior.
Citations are at
f47e9c9ef4.Current behavior
RunnerTests+Models.swift:52-53documentsreadOnlyas "Whether the command is eligible for the session-invalidating retry". The struct comment at:44-48presents the table as three independent axes.RunnerTests+Lifecycle.swift:454), the stopped-app refusalnotRunningReadResponse(:321-325), activation routing (RunnerTests+CommandDispatch.swift:445-451), recorded-failure→session invalidation (:536, jointly withisLifecycle), and remembered-text-entry tap clearing (RunnerTests+CommandExecution.swift:11).querySelectoris classifiedreadOnly: .never(RunnerTests+Models.swift:116, comment at:114). Because the refusal and the activation both read the retry bit, aquerySelectorwhose target app is.notRunningskipsnotRunningReadResponseand reachesactivateTarget(bundleId:reason:"bundle_changed")atCommandDispatch.swift:451. fix(ios): never launch a not-running session app from an observation #2852 established that a runner read must not launch a stopped app.packages/platform-apple/src/runner/runner-command-traits.ts:65).Required behavior
querySelectoragainst a stopped app answersAPP_NOT_RUNNINGand stays non-retried.noApp | existingApp | mayLaunch— resolved in the same exhaustiveCommandType.traitsswitch, and the not-running refusal lives under it. A change to retry eligibility must not be able to change it.Command, removingCommandTraits.ReadOnly.conditionaland thealert-specific resolver atRunnerTests+Lifecycle.swift:465-473. Conditionality is per fact:alertgetpermits read retry,accept/dismissdo not, and everyalertaction skips app-activation preflight (RunnerTests+CommandDispatch.swift:549-551).isLifecycle. The comment atRunnerTests+Models.swift:100-104exists only to explain that collision and should be deleted by this change.Out of scope
Do not force the two trait tables equal. They are homonyms, not twins: TS
readOnlyalso gates readiness preflight (runner-session.ts:1205),waitForRunnerversus a single send (:1034-1045), and transport recovery (runner-command-recovery.ts:247,322).querySelectoranduptimedeliberately allow the TS resend while forbidding the Swift session-loss retry. Genuinely shared facts belong in the table that already exists for them (contracts/fixtures/alert-command-traits.json, read by bothRunnerTests+LifecycleTests.swiftandpackages/platform-apple/src/runner/__tests__/runner-command-traits.test.ts); everything else stays per-side.Completion conditions
querySelectoragainst a stopped app returnsAPP_NOT_RUNNING, and atapin the same state still activates.ReadOnly.conditionaland theModels.swift:100-104comment are gone.CommandTypecase cannot compile without classifying its launch policy.pnpm check:affected --runand the runner XCTest lanes pass on the pushed commit.Dependencies
Found by the #2803 audit. Related: #2881 (request shapes, not trait semantics), #2852, #2862.