@tapflowio/ios-agent 0.28.1 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/NOTICE CHANGED
@@ -9,4 +9,4 @@ licensed under the Apache License, Version 2.0 (see LICENSE-APACHE-2.0).
9
9
  It includes a list of launchd labels selected from simslim
10
10
  (https://github.com/MobAI-App/simslim),
11
11
  licensed under the MIT License. Copyright (c) 2026 Interlap.
12
- The full notice is in dist/leanLabels.js.
12
+ The full notice is in dist/leanLabels.js and dist/serviceTiers.js.
@@ -157,14 +157,22 @@ export declare class IOSAgent implements DeviceAgent, NetworkControlCapability {
157
157
  private abandonBoot;
158
158
  private handleDeviceBoot;
159
159
  private bootWithZombieRecovery;
160
+ /** The tier a boot gets when it does not ask for one: Lean mode's setting. A person's boot is always this. */
161
+ private defaultTier;
162
+ /** What the device boots with now, for the relay: the marker's tier, or `unsupported` on a runtime that cannot take one. */
163
+ private reportedTier;
160
164
  /**
161
165
  * Called only when the device is known to be shut down: `launchd_sim` reads the store when it
162
166
  * starts, and a write while it runs is not known to survive. So a device that was already up when
163
- * asked for is booted as it is, and becomes lean from its next boot through tapflow.
167
+ * asked for is booted as it is, and takes the default tier from its next boot through tapflow —
168
+ * unless a run asked for a tier, which restarts it (`retier` in `handleDeviceBoot`).
164
169
  *
165
- * **A failure here never stops the boot.** Lean mode is an optimisation and booting is the job.
170
+ * **Every boot sets an exact tier**, the asked-for one or the default. Adding to whatever the last boot left
171
+ * is how a device a run left slim reached a person still slim (L2 review R4-S1).
172
+ *
173
+ * **A failure here never stops the boot.** A tier is an optimisation and booting is the job.
166
174
  */
167
- private prepareLean;
175
+ private prepareTier;
168
176
  /** Put back what Lean mode disabled on a device that is now shut down. A no-op for devices it never touched. */
169
177
  private revertLean;
170
178
  /**
@@ -175,6 +183,19 @@ export declare class IOSAgent implements DeviceAgent, NetworkControlCapability {
175
183
  */
176
184
  private revertLeftoverLean;
177
185
  private handleDeviceShutdown;
186
+ /**
187
+ * What `flow run` asks reset before a run, carried out on a booted device. Only the agent's report says it happened.
188
+ * It waits for the system services first: `Booted` shows before tccd and securityd serve, and not only on a boot this
189
+ * agent started — a device someone else just booted reads `booted` too. On a settled device the wait returns at once.
190
+ * The wait does not watch `isStale`; a shutdown meanwhile holds this boot's abandonment until its deadline.
191
+ */
192
+ private resetForRun;
193
+ /**
194
+ * The pasteboard step of a run reset. The queue can be held by a clipboard operation already running, so the wait for
195
+ * it is bounded too; a clear that reaches the front after the step gave up does nothing, because by then the run may
196
+ * have put something of its own there.
197
+ */
198
+ private clearPasteboardQueued;
178
199
  private ensureTouchHelper;
179
200
  private readonly clipboardQueue;
180
201
  private readonly parkedSentinels;
@@ -1 +1 @@
1
- {"version":3,"file":"IOSAgent.d.ts","sourceRoot":"","sources":["../src/IOSAgent.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"IOSAgent.d.ts","sourceRoot":"","sources":["../src/IOSAgent.ts"],"names":[],"mappings":"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"}
package/dist/IOSAgent.js CHANGED
@@ -7,6 +7,8 @@ import { spawn } from 'child_process';
7
7
  import { WebSocket } from 'ws';
8
8
  import { createLogger, PlatformError, ValidationError, bootAbandonMessage, BOOT_NO_SESSION_STATE, SHUTDOWN_NO_SESSION_STATE, downloadBuild } from '@tapflowio/agent-core';
9
9
  const logger = createLogger('ios-agent');
10
+ /** The run reset's pasteboard step, the wait for the clipboard queue included; one step's bound like the others. */
11
+ const RUN_RESET_PASTEBOARD_TIMEOUT_MS = 10_000;
10
12
  // Typed so a typo cannot ship silently — the viewer gates the whole clipboard bridge on this.
11
13
  // `full-reset` is honoured in `handleDeviceBoot`, which shuts a running device down before
12
14
  // `simctl erase` (which refuses anything but a shut-down device) and boots it again.
@@ -39,11 +41,11 @@ export const IOS_BUTTON_ALIASES = {
39
41
  volume_up: 'volume-up',
40
42
  volume_down: 'volume-down',
41
43
  };
42
- import { createResourceSampler, registerStreamWs, disableNagle, createKeyframeAwareSender, pickMaxSize, createRateLimitedDropWarn, createThroughputSampler, createSleepBlocker, acquireLoopStallWatch, describeRelayLoss, getMachineId, isLocalhostWss, DEFAULT_BACKPRESSURE_BYTES, writeEnvelopeHeader, rewriteLowLatencySpsInFrame, CODEC_JPEG, CODEC_H264, CODEC_AUDIO, sendAudioYieldingToVideo, } from '@tapflowio/agent-core/utils';
44
+ import { createResourceSampler, registerStreamWs, disableNagle, createKeyframeAwareSender, pickMaxSize, createRateLimitedDropWarn, createThroughputSampler, createSleepBlocker, acquireLoopStallWatch, describeRelayLoss, getMachineId, sameServiceTier, resetApps, runResetSteps, RUN_RESET_BUDGET_MS, isLocalhostWss, DEFAULT_BACKPRESSURE_BYTES, writeEnvelopeHeader, rewriteLowLatencySpsInFrame, CODEC_JPEG, CODEC_H264, CODEC_AUDIO, sendAudioYieldingToVideo, } from '@tapflowio/agent-core/utils';
43
45
  import { SimctlWrapper, isDeviceMissingError, ClipboardTooLargeError, firstLine, launchFailureReason } from './SimctlWrapper.js';
44
46
  import { SimulatorNetwork } from './SimulatorNetwork.js';
45
47
  import { LeanStore } from './LeanStore.js';
46
- import { LEAN_LABELS } from './leanLabels.js';
48
+ import { MANAGED_LABELS, labelsFor } from './serviceTiers.js';
47
49
  import { MAX_CLIPBOARD_BYTES, clipboardByteLength, CLIPBOARD_SENTINEL_PREFIX as SENTINEL_PREFIX, isClipboardSentinel as isSentinel, CLIPBOARD_COPY_DEADLINE_MS, CLIPBOARD_WRITE_DEADLINE_MS, CLIPBOARD_RESTORE_DEADLINE_MS, CLIPBOARD_POLL_MS, createKeyedSerialQueue, } from '@tapflowio/agent-core';
48
50
  import { ScreenCaptureStreamer } from './ScreenCaptureStreamer.js';
49
51
  import { AudioCaptureStreamer, readSimVolume, applyGain } from './AudioCaptureStreamer.js';
@@ -182,7 +184,7 @@ export class IOSAgent {
182
184
  // reason, as `sleepBlocker` below.
183
185
  this.lean = options.lean ?? false;
184
186
  // Never the host's real store under vitest, for the reason given for `network` just below.
185
- this.leanStore = options.leanStore ?? new LeanStore(process.env.VITEST ? path.join(tmpdir(), 'tapflow-no-lean-store') : undefined);
187
+ this.leanStore = options.leanStore ?? new LeanStore(process.env.VITEST ? path.join(tmpdir(), 'tapflow-no-lean-store') : undefined, MANAGED_LABELS);
186
188
  this.network = options.network ?? new SimulatorNetwork(this.simctl, process.env.VITEST ? { filterHostBinary: path.join(tmpdir(), 'tapflow-no-filter-host') } : {});
187
189
  // **Wired here rather than in the constructor call above, and the difference is coverage.** An
188
190
  // injected `SimulatorNetwork` — which is what every test uses — would otherwise never carry the
@@ -264,6 +266,7 @@ export class IOSAgent {
264
266
  status: d.status,
265
267
  osVersion: d.osVersion,
266
268
  formFactor: d.typeId ? formFactors.get(d.typeId) : undefined,
269
+ serviceTier: this.reportedTier(d),
267
270
  })),
268
271
  });
269
272
  });
@@ -446,6 +449,7 @@ export class IOSAgent {
446
449
  slotsAvailable: Math.max(0, slotsTotal - bootedCount),
447
450
  slotsTotal,
448
451
  reportedAt: Date.now(),
452
+ memoryPressure: this.resources.getMemoryPressure(),
449
453
  },
450
454
  });
451
455
  }
@@ -674,7 +678,7 @@ export class IOSAgent {
674
678
  // `requestId` is a parameter, never a field on `state`: `bootSeq` exists because two boots overlap,
675
679
  // and a correlator hoisted onto shared state would answer the first request with the second's id.
676
680
  // Optional because the relay's idle timer boots nothing — but every *browser* boot carries one.
677
- async handleDeviceBoot(sessionId, deviceId, fullErase = false, acceptH264 = false, tier, requestId, automation = false) {
681
+ async handleDeviceBoot(sessionId, deviceId, fullErase = false, acceptH264 = false, tier, requestId, automation = false, serviceTier, runReset) {
678
682
  const state = this.deviceStates.get(sessionId);
679
683
  // Split, because the two halves are not the same kind of nothing. No open control channel means the
680
684
  // answer itself has nowhere to go, so this is the one abandonment that stays silent — and the caller
@@ -721,6 +725,13 @@ export class IOSAgent {
721
725
  const target = devices.find((d) => d.id === deviceId);
722
726
  if (!target)
723
727
  throw new PlatformError(`Device not found: ${deviceId}`);
728
+ // A running device on another tier than the run asked for is restarted onto it: the services have to be off
729
+ // from launch, and replaying on whatever the last boot left is what the tier exists to stop.
730
+ const retier = !fullErase && target.status === 'booted' && serviceTier !== undefined && leanSupported(target.osVersion)
731
+ && !sameServiceTier(this.leanStore.currentTier(deviceId), serviceTier);
732
+ // Read before anything below changes it: a Full reset shuts the device down itself, and `unknown`
733
+ // (booting, shutting down) is a device paying a boot's cost now.
734
+ const cold = fullErase || retier || target.status !== 'booted';
724
735
  if (fullErase) {
725
736
  // `simctl erase` only accepts a shut-down device, and a device is often already running —
726
737
  // it survives agent restarts and sessions that ended without a clean shutdown. Without this
@@ -747,7 +758,6 @@ export class IOSAgent {
747
758
  this.abandonBoot(state, seq, sessionId, requestId);
748
759
  return;
749
760
  }
750
- this.prepareLean(target);
751
761
  try {
752
762
  await this.simctl.erase(deviceId);
753
763
  }
@@ -770,6 +780,9 @@ export class IOSAgent {
770
780
  }
771
781
  throw err;
772
782
  }
783
+ // After the erase, not before: a failed erase boots the device back as it was found, and the tier is part of
784
+ // that. The overrides live on the host, so the erase does not touch them either way.
785
+ this.prepareTier(target, serviceTier);
773
786
  await this.simctl.boot(deviceId);
774
787
  }
775
788
  else {
@@ -780,10 +793,22 @@ export class IOSAgent {
780
793
  // deadline to discover. `SimctlWrapper.boot` swallows `Unable to boot device in current
781
794
  // state: Booted`, so re-issuing it for a device that really is up costs one no-op
782
795
  // subprocess and makes "the wait only ever runs after a boot was accepted" true.
783
- if (target.status === 'shutdown')
784
- this.prepareLean(target);
785
- else if (this.lean && !this.leanStore.isApplied(deviceId)) {
786
- logger.info(`Lean mode: ${target.name} is already running, so it applies from its next boot`);
796
+ if (retier) {
797
+ logger.info(`Service tier: restarting ${target.name} onto ${serviceTier.name}`);
798
+ // As for a Full reset: a runner starting for this device would boot it back up mid-change.
799
+ this.uiTreeReader.stopIfDevice(deviceId);
800
+ await this.simctl.shutdown(deviceId);
801
+ if (seq !== state.bootSeq) {
802
+ this.abandonBoot(state, seq, sessionId, requestId);
803
+ return;
804
+ }
805
+ this.prepareTier(target, serviceTier);
806
+ }
807
+ else if (target.status === 'shutdown') {
808
+ this.prepareTier(target, serviceTier);
809
+ }
810
+ else if (leanSupported(target.osVersion) && !sameServiceTier(this.leanStore.currentTier(deviceId), this.defaultTier())) {
811
+ logger.info(`Service tier: ${target.name} is already running, so ${this.defaultTier().name} applies from its next boot`);
787
812
  }
788
813
  await this.bootWithZombieRecovery(deviceId);
789
814
  }
@@ -802,6 +827,13 @@ export class IOSAgent {
802
827
  // twice a second against a device that is now deliberately off — for the rest of the deadline,
803
828
  // with nothing left that wants the answer.
804
829
  const bootedDevice = await this.simctl.waitUntilBooted(deviceId, { isStale: () => seq !== state.bootSeq });
830
+ // Before anything runs on the device: the network arm below starts nothing, and the run installs its app after the
831
+ // ready. Clearing an app now and installing it after leaves a clean container.
832
+ const runResetReport = runReset ? await this.resetForRun(deviceId, runReset, () => seq !== state.bootSeq) : undefined;
833
+ if (seq !== state.bootSeq) {
834
+ this.abandonBoot(state, seq, sessionId, requestId);
835
+ return;
836
+ }
805
837
  // Arm the network injection while nothing is running on the device yet — dyld reads the
806
838
  // environment at process start, so anything launched before this is unhooked for its whole life.
807
839
  // Best-effort: the network toggle is opt-in and a device that cannot be armed says so through
@@ -847,7 +879,7 @@ export class IOSAgent {
847
879
  // every path precisely so that case reaches here. Requiring an off→on transition would leave
848
880
  // that device unowned and put the resolvers back to refusing between two live simulators.
849
881
  this.ownedDevices.add(deviceId);
850
- this.sendMsg({ type: 'device:ready', sessionId, requestId, payload: { deviceId } });
882
+ this.sendMsg({ type: 'device:ready', sessionId, requestId, payload: { deviceId, cold, serviceTier: this.reportedTier(target), ...(runResetReport ? { runReset: runResetReport } : {}) } });
851
883
  // The unsolicited report the protocol names on `device:ready`. It follows the ready rather than
852
884
  // riding inside it: a tester whose device just came up can arm the toggle in the same breath, and
853
885
  // a boot that armed nothing still has to say so rather than leaving the control blank.
@@ -897,27 +929,37 @@ export class IOSAgent {
897
929
  }
898
930
  }
899
931
  // ── Lean mode ──────────────────────────────────────────────────────────────
932
+ /** The tier a boot gets when it does not ask for one: Lean mode's setting. A person's boot is always this. */
933
+ defaultTier() {
934
+ return { name: this.lean ? 'lean' : 'stock', keep: [] };
935
+ }
936
+ /** What the device boots with now, for the relay: the marker's tier, or `unsupported` on a runtime that cannot take one. */
937
+ reportedTier(device) {
938
+ return leanSupported(device.osVersion) ? this.leanStore.currentTier(device.id) : 'unsupported';
939
+ }
900
940
  /**
901
941
  * Called only when the device is known to be shut down: `launchd_sim` reads the store when it
902
942
  * starts, and a write while it runs is not known to survive. So a device that was already up when
903
- * asked for is booted as it is, and becomes lean from its next boot through tapflow.
943
+ * asked for is booted as it is, and takes the default tier from its next boot through tapflow —
944
+ * unless a run asked for a tier, which restarts it (`retier` in `handleDeviceBoot`).
904
945
  *
905
- * **A failure here never stops the boot.** Lean mode is an optimisation and booting is the job.
946
+ * **Every boot sets an exact tier**, the asked-for one or the default. Adding to whatever the last boot left
947
+ * is how a device a run left slim reached a person still slim (L2 review R4-S1).
948
+ *
949
+ * **A failure here never stops the boot.** A tier is an optimisation and booting is the job.
906
950
  */
907
- prepareLean(device) {
951
+ prepareTier(device, requested) {
952
+ const tier = requested ?? this.defaultTier();
908
953
  try {
909
- if (!this.lean) {
910
- this.revertLean(device.id);
911
- }
912
- else if (!leanSupported(device.osVersion)) {
913
- logger.info(`Lean mode: not applied to ${device.name} (${device.osVersion ?? 'unknown runtime'}) — iOS 18.5 or later only`);
914
- }
915
- else {
916
- this.leanStore.apply(device.id, LEAN_LABELS);
954
+ if (!leanSupported(device.osVersion)) {
955
+ if (tier.name !== 'stock')
956
+ logger.info(`Service tier: ${tier.name} not applied to ${device.name} (${device.osVersion ?? 'unknown runtime'}) — iOS 18.5 or later only`);
957
+ return;
917
958
  }
959
+ this.leanStore.setTier(device.id, tier, labelsFor(tier));
918
960
  }
919
961
  catch (e) {
920
- logger.warn(`Lean mode: could not update ${device.name}, booting it as it is:`, e.message);
962
+ logger.warn(`Service tier: could not set ${device.name} to ${tier.name}, booting it as it is:`, e.message);
921
963
  }
922
964
  }
923
965
  /** Put back what Lean mode disabled on a device that is now shut down. A no-op for devices it never touched. */
@@ -1014,6 +1056,49 @@ export class IOSAgent {
1014
1056
  });
1015
1057
  }
1016
1058
  }
1059
+ /**
1060
+ * What `flow run` asks reset before a run, carried out on a booted device. Only the agent's report says it happened.
1061
+ * It waits for the system services first: `Booted` shows before tccd and securityd serve, and not only on a boot this
1062
+ * agent started — a device someone else just booted reads `booted` too. On a settled device the wait returns at once.
1063
+ * The wait does not watch `isStale`; a shutdown meanwhile holds this boot's abandonment until its deadline.
1064
+ */
1065
+ async resetForRun(deviceId, request, isStale) {
1066
+ // One budget for the wait and the steps: a device whose services never come up spends it waiting, and the steps
1067
+ // that would have failed against it are reported failed instead of each running out its own timeout.
1068
+ const deadline = Date.now() + RUN_RESET_BUDGET_MS;
1069
+ const stop = () => isStale() || Date.now() >= deadline;
1070
+ await this.simctl.waitForBootStatus(deviceId).catch((e) => logger.warn(`run reset: boot status unknown: ${firstLine(e)}`));
1071
+ const steps = [];
1072
+ if (request.uninstall.length > 0) {
1073
+ steps.push({ item: 'app-uninstall', run: () => resetApps(request.uninstall, (id) => this.simctl.uninstallAppForReset(deviceId, id), stop) });
1074
+ }
1075
+ if (request.clear.length > 0) {
1076
+ steps.push({ item: 'app-files', run: () => resetApps(request.clear, (id) => this.simctl.clearAppDataIfInstalled(deviceId, id), stop) });
1077
+ }
1078
+ steps.push({ item: 'keychain', run: () => this.simctl.resetKeychain(deviceId) }, { item: 'privacy-services', run: () => this.simctl.resetPrivacy(deviceId) },
1079
+ // Through the clipboard queue: a clipboard read in flight parks a sentinel on this pasteboard.
1080
+ { item: 'pasteboard', run: () => this.clearPasteboardQueued(deviceId, stop) }, { item: 'location', run: () => this.simctl.clearLocation(deviceId) });
1081
+ return runResetSteps(steps, isStale, (item, e) => logger.warn(`run reset: ${item} failed: ${firstLine(e)}`), deadline);
1082
+ }
1083
+ /**
1084
+ * The pasteboard step of a run reset. The queue can be held by a clipboard operation already running, so the wait for
1085
+ * it is bounded too; a clear that reaches the front after the step gave up does nothing, because by then the run may
1086
+ * have put something of its own there.
1087
+ */
1088
+ clearPasteboardQueued(deviceId, stop) {
1089
+ let abandoned = false;
1090
+ let timer;
1091
+ const queued = this.clipboardQueue(deviceId, async () => {
1092
+ if (abandoned || stop())
1093
+ throw new PlatformError('the clipboard was busy for the whole reset');
1094
+ await this.simctl.setPasteboard(deviceId, '');
1095
+ });
1096
+ const late = new Promise((_, reject) => {
1097
+ timer = setTimeout(() => { abandoned = true; reject(new PlatformError('pasteboard clear timed out')); }, RUN_RESET_PASTEBOARD_TIMEOUT_MS);
1098
+ });
1099
+ queued.catch(() => { });
1100
+ return Promise.race([queued, late]).finally(() => clearTimeout(timer));
1101
+ }
1017
1102
  // Lazily set up the touch channel: a reconnect re-issues the session with touchHelper=null while the sim stays booted, so input self-heals without a fresh device:boot. Sync so a tap's start+end stay paired (async would drop touchEnd → stuck finger).
1018
1103
  ensureTouchHelper(state) {
1019
1104
  if (state.touchHelper)
@@ -1149,9 +1234,9 @@ export class IOSAgent {
1149
1234
  switch (msg.type) {
1150
1235
  case 'device:boot': {
1151
1236
  // `external` and `automation` are added by the relay, never by the browser (see `DeviceBoot`).
1152
- const { deviceId, resetMode, acceptH264, secureContext, external, automation } = msg.payload;
1237
+ const { deviceId, resetMode, acceptH264, secureContext, external, automation, serviceTier, runReset } = msg.payload;
1153
1238
  const sessionId = msg.sessionId;
1154
- this.handleDeviceBoot(sessionId, deviceId, resetMode === 'full-erase', acceptH264 === true, { secureContext: !!secureContext, external: !!external }, msg.requestId, automation === true)
1239
+ this.handleDeviceBoot(sessionId, deviceId, resetMode === 'full-erase', acceptH264 === true, { secureContext: !!secureContext, external: !!external }, msg.requestId, automation === true, serviceTier, runReset)
1155
1240
  .catch((e) => logger.error('handleDeviceBoot failed:', e));
1156
1241
  break;
1157
1242
  }
@@ -1256,8 +1341,12 @@ export class IOSAgent {
1256
1341
  // The helper's answer, not its existence: a helper whose process has died reports every
1257
1342
  // write as dropped, and that is what the caller needs to hear (#482).
1258
1343
  const helper = state.touchHelper;
1259
- // terminal of a tap/swipe → ack the gesture
1260
- void this.ackInput(state, helper?.touchEnd() ? 'delivered' : this.refusalReason(helper, 'continuation'), requestId);
1344
+ // terminal of a tap/swipe → ack the gesture, once the up is written (it may wait for the
1345
+ // touch's minimum contact). The refusal is read then too, from the helper the state holds *then*:
1346
+ // one replaced during the wait (a chrome reload, a reboot) is up and owns nothing, which is
1347
+ // `no-gesture`, where the stopped one it replaced would say the channel is gone.
1348
+ void (helper ? helper.touchEndAfterMinContact() : Promise.resolve(false))
1349
+ .then((sent) => this.ackInput(state, sent ? 'delivered' : this.refusalReason(state.touchHelper, 'continuation'), requestId));
1261
1350
  break;
1262
1351
  }
1263
1352
  case 'input:pinch:start': {
@@ -2232,10 +2321,9 @@ export class IOSAgent {
2232
2321
  first?.touchHelper?.touchMove(x, y);
2233
2322
  return Promise.resolve();
2234
2323
  }
2235
- touchEnd() {
2324
+ async touchEnd() {
2236
2325
  const first = this.liveDeviceState();
2237
- first?.touchHelper?.touchEnd();
2238
- return Promise.resolve();
2326
+ await first?.touchHelper?.touchEndAfterMinContact();
2239
2327
  }
2240
2328
  async openUrl(url) {
2241
2329
  return this.simctl.openUrl(await this.soleLiveDeviceId(), url);