|
| 1 | +# ADR 0016: Active-Session Script Publication |
| 2 | + |
| 3 | +## Status |
| 4 | + |
| 5 | +Proposed |
| 6 | + |
| 7 | +## Context |
| 8 | + |
| 9 | +The replay repair study found re-recording a drifted journey cheaper than repairing it in two of the |
| 10 | +three measured drift classes. The immediate reusable unit is an **open-to-destination script**: a |
| 11 | +self-contained `.ad` script that opens the app, performs the complete journey to screen X, verifies a |
| 12 | +destination landmark, and leaves the app session active there so an agent can continue with new work. |
| 13 | + |
| 14 | +Ordinary script recording currently combines two concerns. `open --save-script[=<path>]` arms |
| 15 | +recording before the first interaction so selector chains and ADR 0012 `target-v1` identity evidence |
| 16 | +are captured at action time. `close` publishes the accumulated script, records a terminal `close`, and |
| 17 | +tears down the session. That artifact is suitable as a closed test flow, but not as a live starting |
| 18 | +state. |
| 19 | + |
| 20 | +Publishing only at the end of an ordinary unarmed session is insufficient. Session history retains the |
| 21 | +commands, but the resolved target tree needed for `target-v1` evidence is deliberately discarded after |
| 22 | +each action. Late publication can serialize selector text; it cannot reconstruct which element was |
| 23 | +actually acted on. Making identity capture unconditional for every session would change normal |
| 24 | +interaction execution: recording disables direct selector fast paths when a capture-backed route is |
| 25 | +required for evidence. That performance and data-retention change has not been measured. |
| 26 | + |
| 27 | +Issue [#1346](https://github.com/callstack/agent-device/issues/1346) and the throwaway |
| 28 | +[`session-save-script` prototype](https://github.com/callstack/agent-device/tree/agent/prototype-session-save-replay/scripts/prototypes/session-save-script) |
| 29 | +record the motivating workflow and API trial. Composable lifecycle-free fragments remain a separate |
| 30 | +decision in [#1336](https://github.com/callstack/agent-device/issues/1336). |
| 31 | + |
| 32 | +## Decision |
| 33 | + |
| 34 | +Add an explicit publication action for an already-armed ordinary script recording: |
| 35 | + |
| 36 | +```sh |
| 37 | +agent-device open com.example.app --relaunch --save-script=screen-x.ad |
| 38 | +# perform the complete journey to screen X |
| 39 | +agent-device wait 'role="heading" label="Screen X"' |
| 40 | +agent-device session save-script |
| 41 | +``` |
| 42 | + |
| 43 | +`session save-script [path] [--force]` publishes the current ordinary script recording without |
| 44 | +closing the app or deleting the session. An explicit `path` retargets the recording using the existing |
| 45 | +target/force authorization rules. Without it, publication uses the path armed by `open --save-script` |
| 46 | +or the existing generated default. |
| 47 | + |
| 48 | +### Recording lifecycle |
| 49 | + |
| 50 | +An ordinary recording eligible for active-session publication has three states: |
| 51 | + |
| 52 | +- **ARMED**: established only when a new session's initial successful |
| 53 | + `open --save-script[=<path>]` is recorded as action zero. The session records portable action inputs |
| 54 | + and fresh target identity evidence. |
| 55 | +- **ABORTED**: reached when another plain `open` succeeds while ARMED. The app operation may continue, |
| 56 | + but the recording is no longer a single-open bootstrap and close-time publication is disarmed. The |
| 57 | + successful `open` response warns that a fresh session is required to author another script. |
| 58 | +- **PUBLISHED**: reached only after `session save-script` atomically commits the complete history from |
| 59 | + the sole recorded `open` through the current action. The session remains active at the destination, |
| 60 | + but close-time script publication is disarmed. |
| 61 | + |
| 62 | +A filesystem or target-collision failure leaves the recording ARMED, including its path and same-target |
| 63 | +`--force` authorization, so the caller can correct the target or permissions and retry. ABORTED and |
| 64 | +PUBLISHED are terminal until the session is destroyed. `session save-script` in either state fails |
| 65 | +loudly and never writes. |
| 66 | + |
| 67 | +Every existing arming entry point respects terminality. `open --save-script` on any existing session — |
| 68 | +unarmed, ARMED, ABORTED, or PUBLISHED — is rejected before app dispatch; a plain later `open` is allowed, |
| 69 | +causing ARMED to become ABORTED while leaving ABORTED/PUBLISHED unchanged. |
| 70 | +`close --save-script[=<path>]` in ABORTED or PUBLISHED is rejected before platform close or filesystem |
| 71 | +work, so the caller can retry with plain `close`. Plain `close` tears down ABORTED/PUBLISHED without |
| 72 | +writing; closing an unpublished ARMED recording retains the existing close-time publication behavior. A |
| 73 | +fresh session is the only re-arming boundary. |
| 74 | + |
| 75 | +This lifecycle is distinct from ADR 0012's repair transaction. `session save-script` rejects a session |
| 76 | +with `saveScriptBoundary` set and directs the caller to finish or abort the repair through its existing |
| 77 | +`replay --from` and teardown commit protocol. Active-session publication never marks a repair COMPLETE, |
| 78 | +commits a healed slice, writes `# agent-device:heal-complete`, or changes repair tombstone semantics. |
| 79 | + |
| 80 | +### Destination readiness and replay handoff |
| 81 | + |
| 82 | +The destination is an authored postcondition, not the last navigation action. Before publication, the |
| 83 | +recorded suffix after the last mutating action must contain a `wait` whose target is a portable selector |
| 84 | +or selector chain identifying a landmark on the ready destination screen. A duration wait, `wait stable`, |
| 85 | +or `wait @ref` does not qualify, though `wait stable` may follow the landmark wait. The publisher validates |
| 86 | +the serialized guard before filesystem work and never relies on repair-only bare-ref rejection. |
| 87 | +`session save-script` refuses publication without this **destination guard** and tells the author to |
| 88 | +record one. V1 does not infer a screen identity from a snapshot or synthesize an implicit guard. |
| 89 | + |
| 90 | +The initial guard is selector-level. `wait` does not yet carry recorded-landmark identity through its |
| 91 | +polling resolution, so the guard proves that an element matching its selector exists, not that it is the |
| 92 | +same landmark element observed while authoring. Authors must choose a selective destination-specific |
| 93 | +landmark; a reshuffled screen containing the same weak label elsewhere can false-pass. |
| 94 | +[#1349](https://github.com/callstack/agent-device/issues/1349) owns the identity design for waits and |
| 95 | +remaining read-only steps. It must preserve polling: ADR 0012's current pre-action `target-v1` |
| 96 | +verification cannot be attached to a wait unchanged because a not-yet-present landmark is the expected |
| 97 | +starting condition. Any identity check for a wait happens after its selector resolves, before the wait |
| 98 | +reports success, or uses a distinct guard-specific mechanism. |
| 99 | + |
| 100 | +The phrase "last mutating action" is derived from a request-sensitive recording-effect trait on the |
| 101 | +central `CommandDescriptor`, required for every command that records session actions and guarded by a |
| 102 | +completeness test. It is not a publisher-local command-name set. The trait distinguishes app-state |
| 103 | +mutation from observation for subcommands such as read-only versus mutating `find`, `keyboard`, and |
| 104 | +`alert` actions; the existing conservative `refFrameEffect: may-invalidate` classification is not precise |
| 105 | +enough for this boundary. |
| 106 | + |
| 107 | +On consumption, a script without `close` preserves the existing replay behavior: the named session stays |
| 108 | +active and the successful `ReplayCommandResult` returns its `session` id. The caller binds subsequent |
| 109 | +commands to that returned id. Replay reports success only after the destination guard completes; the |
| 110 | +absence of `close` changes neither action dispatch nor the success response shape. |
| 111 | + |
| 112 | +### Sensitive inputs |
| 113 | + |
| 114 | +Executable `.ad` artifacts serialize action inputs literally. A recorded `fill` therefore writes its |
| 115 | +text to the published file; diagnostic and event-log redaction cannot protect an input that the replay |
| 116 | +engine must later execute. V1 does not claim secret-bearing login flows are safe to publish. |
| 117 | + |
| 118 | +Native `.ad` replay already supports late-bound `${VAR}` values, but ordinary recording cannot yet |
| 119 | +execute with a real value while publishing only its placeholder. That safe-authoring capability is |
| 120 | +tracked in [#1348](https://github.com/callstack/agent-device/issues/1348). Until it ships, authors must not |
| 121 | +record a journey that enters a secret: use pre-authenticated test state or deliberately non-secret fixture |
| 122 | +credentials that are safe to persist. CLI help must state this warning next to the authoring workflow. |
| 123 | + |
| 124 | +### Artifact contract |
| 125 | + |
| 126 | +The published `.ad`: |
| 127 | + |
| 128 | +- contains exactly one recorded `open` as its first action and every recordable action through the |
| 129 | + publication request; |
| 130 | +- contains a portable selector/selector-chain destination guard after its last descriptor-classified |
| 131 | + mutating action; |
| 132 | +- does not append or serialize `session save-script` or `close`; |
| 133 | +- uses the ordinary session context header, selector-chain optimization, and canonical `target-v1` |
| 134 | + annotations captured while ARMED; |
| 135 | +- fails loudly rather than emitting an unresolved session-local `@ref` or dropping target evidence that |
| 136 | + ADR 0012 requires for an element-targeting recorded action; and |
| 137 | +- uses the existing same-directory atomic publication primitive, refusing every existing target unless |
| 138 | + `--force` authorizes atomic replacement. |
| 139 | + |
| 140 | +The success response identifies the final path and session and reports the number of serialized actions. |
| 141 | +The command must fail before writing when there is no active session, recording was not armed, the |
| 142 | +recording is ABORTED or PUBLISHED, the history does not contain exactly one initial `open`, no portable |
| 143 | +destination guard exists, or a repair transaction owns the session. Every failure explains the recovery |
| 144 | +action; none degrades to `{ written: false }` success. |
| 145 | + |
| 146 | +### Surface and naming |
| 147 | + |
| 148 | +V1 extends the existing `session` command and typed session client surface. It does not introduce |
| 149 | +`script start/stop`, marks, or a second replay engine. CLI help makes the two phases explicit: the |
| 150 | +existing `--save-script` flag arms evidence capture, while `session save-script` publishes without |
| 151 | +teardown. |
| 152 | + |
| 153 | +This ADR does not rename or deprecate `--save-script`. The flag and session action name the same persisted |
| 154 | +artifact: `open --save-script` configures the armed recording's eventual target, while |
| 155 | +`session save-script` requests publication now. `save-replay` is rejected because replay is the act of |
| 156 | +executing that script, not the artifact being saved. |
| 157 | + |
| 158 | +## Consequences |
| 159 | + |
| 160 | +- Agents can record onboarding or deep navigation as one self-contained starting state, replay it from |
| 161 | + scratch, and continue from the resulting live session. |
| 162 | +- The workflow has two explicit moments because evidence must be armed before the first target action and |
| 163 | + the destination is known only when the caller publishes. |
| 164 | +- Normal unarmed interactions keep their current fast paths and retention behavior. |
| 165 | +- A successful active-session publication cannot collide with a later close-time auto-save. |
| 166 | +- A second successful open abandons the in-flight artifact instead of silently publishing a multi-open |
| 167 | + bootstrap; authoring resumes only in a fresh session. |
| 168 | +- Intermediate lifecycle-free fragments, entry guards, include semantics, composed digests, and shared |
| 169 | + fragment pinning remain entirely under #1336. |
| 170 | +- Secret-bearing authoring remains unsafe until #1348; the initial workflow is limited to journeys that |
| 171 | + do not enter secrets. Arbitrary history ranges remain out of scope. |
| 172 | + |
| 173 | +## Alternatives Considered |
| 174 | + |
| 175 | +- **Save any session history at the end:** rejected because target identity evidence cannot be |
| 176 | + reconstructed after the interaction and the resulting artifact would undercut ADR 0012's provenance |
| 177 | + model. |
| 178 | +- **Capture full target evidence in every session:** rejected until its direct-path latency, capture |
| 179 | + count, memory, and event-log costs are measured. It would alter ordinary interaction behavior to make |
| 180 | + one authoring command shorter. |
| 181 | +- **`close --no-close` or `--save-script --no-close`:** rejected because a command named for teardown |
| 182 | + would conditionally preserve the session and because it would not solve late arming. |
| 183 | +- **General `script start/stop` or history marks:** rejected because the accepted v1 boundary is exactly |
| 184 | + one recorded `open` through one destination. Arbitrary slices require entry-state semantics and belong |
| 185 | + with fragment design. |
| 186 | +- **`replay save`:** rejected because `replay <path>` consumes an artifact while publication consumes a |
| 187 | + live session; the session owns the source data and lifecycle. |
| 188 | +- **Infer a destination fingerprint at publication:** rejected for v1 because screen identity and |
| 189 | + readiness are app semantics. A caller-authored target wait is explicit, already recordable, and fails |
| 190 | + at the correct point during cold replay. |
| 191 | + |
| 192 | +## Validation Required for Implementation |
| 193 | + |
| 194 | +- An unarmed session refuses publication before filesystem work and names `open --save-script` as the |
| 195 | + recovery. |
| 196 | +- An armed session without a destination guard refuses publication before filesystem work and names a |
| 197 | + selector-targeted `wait` as the recovery; `wait @ref`, duration waits, and `wait stable` are covered |
| 198 | + refusal cases. |
| 199 | +- A second successful plain `open` transitions ARMED to ABORTED and disables all publication, while |
| 200 | + `open --save-script` on an existing session is rejected before app dispatch. |
| 201 | +- An armed session publishes `open` plus target-annotated actions without `close`, returns the final path, |
| 202 | + remains active, and can continue accepting commands. |
| 203 | +- The artifact replays from a cold start, completes its destination guard, returns the live session id, |
| 204 | + and accepts a subsequent command on that session. |
| 205 | +- Every action in ADR 0012's existing target-binding command set has canonical identity evidence and no |
| 206 | + unresolved `@ref` reaches disk; the destination guard remains selector-level until #1349 lands. |
| 207 | +- Existing-target refusal preserves the original bytes; `--force` replaces atomically; a failed publish |
| 208 | + remains retryable. |
| 209 | +- After PUBLISHED, later ordinary actions remain usable, repeated `session save-script` fails, plain |
| 210 | + `open --relaunch` cannot re-arm, and `open --save-script` is rejected before app dispatch. |
| 211 | +- In ABORTED/PUBLISHED, `close --save-script[=<other>]` is rejected before platform close and plain |
| 212 | + `close` tears down without writing; closing an unpublished ARMED recording preserves current |
| 213 | + close-time publication behavior. |
| 214 | +- Descriptor completeness tests classify every recordable request's mutation effect, including |
| 215 | + request-sensitive read-only/mutating subcommands, and destination-guard ordering consumes only that |
| 216 | + trait. |
| 217 | +- Repair-armed sessions refuse this action without changing repair state. |
| 218 | +- CLI help warns that literal `fill` inputs are persisted and tells authors not to record secret-bearing |
| 219 | + journeys until #1348's parameterized-input mechanism is available. |
| 220 | +- Provider-backed integration scenarios cover the public daemon route, and live iOS and Android runs |
| 221 | + prove the saved artifact and post-save session behavior on real backends. |
0 commit comments