@stim-cli/core 1.16.0 → 1.17.1

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/dist/state.mjs CHANGED
@@ -39,20 +39,21 @@ function parseAgentEvents(lines) {
39
39
  const { kind, command, summary } = entry;
40
40
  const requestId = typeof entry.requestId === "string" ? entry.requestId : null;
41
41
  if (kind === "request.started" && requestId && Number.isFinite(ts)) started.push([requestId, ts]);
42
- if (kind !== "action.recorded" && kind !== "request.finished") continue;
42
+ const scopeChange = kind === "request.started" && (command === "open" || command === "close");
43
+ if (kind !== "action.recorded" && kind !== "request.finished" && !scopeChange) continue;
43
44
  const failed = kind === "request.finished" && entry.status === "error";
44
45
  if (kind === "request.finished" && !failed) {
45
46
  if (requestId) finished.push(requestId);
46
47
  continue;
47
48
  }
48
49
  if (!Number.isFinite(ts) || typeof entry.session !== "string" || typeof command !== "string") continue;
49
- if (typeof summary !== "string") continue;
50
+ if (typeof summary !== "string" && !scopeChange) continue;
50
51
  events.push({
51
52
  ts,
52
53
  session: entry.session,
53
54
  kind,
54
55
  command,
55
- summary,
56
+ summary: typeof summary === "string" ? summary : "",
56
57
  failed,
57
58
  details: object$1(entry.details),
58
59
  requestId
@@ -147,11 +148,14 @@ function readCompleteLines(path, start) {
147
148
  let fd;
148
149
  try {
149
150
  fd = openSync(path, "r");
150
- const size = fstatSync(fd).size;
151
+ const stat = fstatSync(fd);
152
+ const size = stat.size;
153
+ const identity = `${stat.dev}:${stat.ino}`;
151
154
  if (size <= start) return {
152
155
  text: "",
153
156
  next: start,
154
- size
157
+ size,
158
+ identity
155
159
  };
156
160
  const buffer = Buffer.alloc(size - start);
157
161
  const read = readSync(fd, buffer, 0, buffer.length, start);
@@ -159,7 +163,8 @@ function readCompleteLines(path, start) {
159
163
  return {
160
164
  text: end < 0 ? "" : buffer.toString("utf8", 0, end + 1),
161
165
  next: start + end + 1,
162
- size
166
+ size,
167
+ identity
163
168
  };
164
169
  } catch {
165
170
  return null;
@@ -167,16 +172,23 @@ function readCompleteLines(path, start) {
167
172
  if (fd !== void 0) closeSync(fd);
168
173
  }
169
174
  }
175
+ function trackMacosOpen(cursor, event, unknownVersion) {
176
+ const flags = object$1(event.details?.flags);
177
+ if (!event.failed) cursor.macos = event.details?.platform === "macos" || flags?.platform === "macos";
178
+ cursor.bundleId = cursor.macos && !event.failed && !unknownVersion && flags?.surface === "app" && typeof event.details?.appBundleId === "string" ? event.details.appBundleId : null;
179
+ }
170
180
  /**
171
181
  * Reads agent-device session actions incrementally from the supplied sessions directories. With targets,
172
182
  * records are restricted to those devices; without targets, retained session metadata supplies attribution.
173
183
  * Missing directories and unreadable files yield no records.
174
184
  */
175
- function createAgentActionReader({ sessionsDirs, targets, sinceTs = -Infinity, now = Date.now, claimedDevice = () => null }) {
185
+ function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceTs = -Infinity, now = Date.now, claimedDevice = () => null }) {
176
186
  const cursors = /* @__PURE__ */ new Map();
177
- const byId = new Map(targets?.map((target) => [target.id, target]));
178
187
  return () => {
188
+ const targets = typeof resolveTargets === "function" ? resolveTargets() : resolveTargets;
179
189
  if (targets && !targets.length) return [];
190
+ const byId = new Map(targets?.map((target) => [target.id, target]));
191
+ const macosByBundle = new Map(targets?.flatMap((target) => target.platform === "macos" ? [[target.bundleId, target]] : []));
180
192
  const sessionDirs = (typeof sessionsDirs === "function" ? sessionsDirs() : sessionsDirs).flatMap((sessionsDir) => {
181
193
  try {
182
194
  return readdirSync(sessionsDir).map((name) => ({
@@ -205,7 +217,10 @@ function createAgentActionReader({ sessionsDirs, targets, sinceTs = -Infinity, n
205
217
  runner: null,
206
218
  session: null,
207
219
  unknownReported: false,
208
- started: /* @__PURE__ */ new Map()
220
+ started: /* @__PURE__ */ new Map(),
221
+ macos: false,
222
+ bundleId: null,
223
+ closing: null
209
224
  };
210
225
  cursors.set(dir, cursor);
211
226
  try {
@@ -213,8 +228,12 @@ function createAgentActionReader({ sessionsDirs, targets, sinceTs = -Infinity, n
213
228
  } catch {}
214
229
  }
215
230
  let chunk = readCompleteLines(eventsPath, cursor.offset);
216
- if (chunk && chunk.size < cursor.offset) chunk = readCompleteLines(eventsPath, 0);
231
+ if (chunk && (chunk.size < cursor.offset || cursor.identity && chunk.identity !== cursor.identity)) {
232
+ cursor.bundleId = null;
233
+ chunk = readCompleteLines(eventsPath, 0);
234
+ }
217
235
  if (!chunk) continue;
236
+ cursor.identity = chunk.identity;
218
237
  cursor.offset = chunk.next;
219
238
  lines.push(...chunk.text.split("\n"));
220
239
  const parsed = parseAgentEvents(lines);
@@ -241,7 +260,7 @@ function createAgentActionReader({ sessionsDirs, targets, sinceTs = -Infinity, n
241
260
  spans: parseRunnerSpans(text)
242
261
  };
243
262
  }
244
- for (const event of parsed.events) if (event.command === "close" && !event.failed) cursor.closes.push(event.ts);
263
+ for (const event of parsed.events) if (event.command === "close" && event.kind === "action.recorded") cursor.closes.push(event.ts);
245
264
  const spans = sessionSpans(cursor.runner.spans, cursor.closes);
246
265
  const deviceAt = (ts) => spans.length ? spanDevice(spans, ts) : session && claimedDevice(session, sessionsDir);
247
266
  cursor.platform ??= parsed.events.map(platformOf).find((platform) => platform !== void 0);
@@ -252,13 +271,30 @@ function createAgentActionReader({ sessionsDirs, targets, sinceTs = -Infinity, n
252
271
  });
253
272
  if (parsed.unknownVersion && !cursor.unknownReported) {
254
273
  const deviceId = spans.at(-1)?.deviceId ?? (session && claimedDevice(session, sessionsDir));
255
- const target = targets ? deviceId ? byId.get(deviceId) : void 0 : retainedTarget();
274
+ const target = !targets ? retainedTarget() : cursor.macos ? cursor.bundleId ? macosByBundle.get(cursor.bundleId) : void 0 : deviceId ? byId.get(deviceId) : void 0;
256
275
  if (target) {
257
276
  cursor.unknownReported = true;
258
277
  out.push(formatUnknownRecord(session ?? name, parsed.unknownVersion, target, now()));
259
278
  }
260
279
  }
280
+ if (parsed.unknownVersion) cursor.bundleId = null;
261
281
  for (const event of parsed.events) {
282
+ if (event.kind === "request.started") {
283
+ cursor.closing = event.command === "close" ? cursor.bundleId : null;
284
+ cursor.bundleId = null;
285
+ continue;
286
+ }
287
+ if (targets && event.command === "open") trackMacosOpen(cursor, event, Boolean(parsed.unknownVersion));
288
+ if (targets && cursor.macos) {
289
+ if (event.command === "screenshot") continue;
290
+ const bundleId = cursor.bundleId ?? (event.command === "close" ? cursor.closing : null);
291
+ cursor.closing = null;
292
+ const target = bundleId ? macosByBundle.get(bundleId) : void 0;
293
+ const startedAt = event.requestId ? matched.get(event.requestId) : void 0;
294
+ if (target?.platform === "macos" && event.ts >= Math.max(sinceTs, target.launchedAt) && (startedAt ?? event.ts) >= target.launchedAt) out.push(agentRecord(event, target.id, target, startedAt));
295
+ if (event.command === "close") cursor.bundleId = null;
296
+ continue;
297
+ }
262
298
  if (event.ts < sinceTs) continue;
263
299
  if (!targets) cursor.platform = platformOf(event) ?? cursor.platform;
264
300
  const deviceId = !targets && cursor.platform === "android" ? null : deviceAt(event.ts);
@@ -452,14 +488,14 @@ function agentSessionsCacheFile() {
452
488
  function diskUsageCacheDir() {
453
489
  return join(configDir(), "disk-usage");
454
490
  }
455
- /** The build machines this Mac paired with, each with its pinned tailnet node and device token. */
491
+ /** The remote Macs this Mac paired with, each with its pinned tailnet node and device token. */
456
492
  function buildMachinesFile() {
457
493
  return join(configDir(), "build-machines.json");
458
494
  }
459
495
  function buildMachinesLock() {
460
496
  return join(configDir(), "build-machines.lock");
461
497
  }
462
- /** The hosting machines this Mac asked for access, separate from build credentials. */
498
+ /** The remote Macs this Mac asked for access, separate from build credentials. */
463
499
  function deviceHostMachinesFile() {
464
500
  return join(configDir(), "device-host-machines.json");
465
501
  }
@@ -539,9 +575,9 @@ function configCorrupt(reason, path = getConfigPath()) {
539
575
  corrupt.code = "STIM_CONFIG_CORRUPT";
540
576
  return corrupt;
541
577
  }
542
- /** The build worker root: `offload.workerRoot` when it is absolute, else `$STIM_HOME/build-worker`. */
578
+ /** The build worker root: `server.workerRoot` when it is absolute, else `$STIM_HOME/build-worker`. */
543
579
  function buildWorkerRoot(config = loadConfig()) {
544
- const configured = config?.offload?.workerRoot;
580
+ const configured = config?.server?.workerRoot;
545
581
  return typeof configured === "string" && configured.startsWith("/") ? configured : join(configDir(), "build-worker");
546
582
  }
547
583
  function loadConfig() {
@@ -591,6 +627,46 @@ function getConcurrencyLimits({ env = process.env } = {}) {
591
627
  maxDevices: resolveLimit(env.STIM_MAX_DEVICES, c.maxDevices)
592
628
  };
593
629
  }
630
+ const MAX_SETTING = {
631
+ ios: {
632
+ key: "iosParkedMax",
633
+ env: "STIM_POOL_IOS_PARKED_MAX"
634
+ },
635
+ android: {
636
+ key: "androidParkedMax",
637
+ env: "STIM_POOL_ANDROID_PARKED_MAX"
638
+ }
639
+ };
640
+ function parseMax(raw, strings) {
641
+ const value = strings && typeof raw === "string" ? /^\d+$/.test(raw.trim()) ? Number(raw.trim()) : NaN : raw;
642
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return null;
643
+ return value;
644
+ }
645
+ function getParkedMax(platform, { config, env = process.env } = {}) {
646
+ const { key, env: envKey } = MAX_SETTING[platform];
647
+ const fromEnv = env[envKey];
648
+ const explicit = fromEnv !== void 0 && fromEnv !== "";
649
+ const pool = (config === void 0 ? loadConfig() : config)?.pool;
650
+ if (!explicit && pool !== void 0 && (pool === null || typeof pool !== "object" || Array.isArray(pool))) return {
651
+ max: 0,
652
+ error: "Invalid pool value. Expected an object with simulator and emulator bounds."
653
+ };
654
+ const fromConfig = pool !== null && typeof pool === "object" && !Array.isArray(pool) ? pool[key] : void 0;
655
+ const raw = explicit ? fromEnv : fromConfig;
656
+ if (raw === void 0) return {
657
+ max: env.STIM_HOME ? 0 : 3,
658
+ error: null
659
+ };
660
+ const parsed = parseMax(raw, explicit);
661
+ if (parsed === null) return {
662
+ max: 0,
663
+ error: `Invalid ${explicit ? envKey : `pool.${key}`} value ${JSON.stringify(raw)}. Expected a whole number of parked ${platform === "ios" ? "simulators" : "emulators"}, 0 or more.`
664
+ };
665
+ return {
666
+ max: explicit ? parsed : env.STIM_HOME ? 0 : parsed,
667
+ error: null
668
+ };
669
+ }
594
670
  function resolveLimit(envVal, cfgVal) {
595
671
  const raw = envVal !== void 0 && envVal !== null && envVal !== "" ? Number(envVal) : typeof cfgVal === "number" ? cfgVal : NaN;
596
672
  if (!Number.isFinite(raw) || raw <= 0) return 0;
@@ -735,11 +811,11 @@ const AGENT_PATH = /^\/device-host\/agent\/[a-f0-9-]{36}\/$/;
735
811
  const LEASE_NAME = /^[A-Za-z0-9._-]{1,128}$/;
736
812
  const LEASE_DEVICE_KEY = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+@[1-9][0-9]{0,9}$/;
737
813
  function parseHostedAgentLease(value) {
738
- if (!isJsonObject(value) || Object.keys(value).length !== (value.backend === "ios-instance" ? 5 : 4) || ![
814
+ if (!isJsonObject(value) || Object.keys(value).length !== (value.backend === "ios-instance" || value.backend === "android-instance" ? 5 : 4) || ![
739
815
  value.tenant,
740
816
  value.runId,
741
817
  value.clientId
742
- ].every((name) => typeof name === "string" && LEASE_NAME.test(name)) || typeof value.deviceKey !== "string" || !(value.backend === "ios-instance" ? /^ios:mobile:[a-fA-F0-9-]{36}$/.test(value.deviceKey) : value.backend === void 0 && LEASE_DEVICE_KEY.test(value.deviceKey))) return null;
818
+ ].every((name) => typeof name === "string" && LEASE_NAME.test(name)) || typeof value.deviceKey !== "string" || !(value.backend === "ios-instance" ? /^ios:mobile:[a-fA-F0-9-]{36}$/.test(value.deviceKey) : value.backend === "android-instance" ? /^android:mobile:emulator-[0-9]+$/.test(value.deviceKey) : value.backend === void 0 && LEASE_DEVICE_KEY.test(value.deviceKey))) return null;
743
819
  return value;
744
820
  }
745
821
  /** A grant naming a driver this client does not know is not usable; it parses as null. */
@@ -852,10 +928,6 @@ const RELATIVE_PATH = {
852
928
  };
853
929
  const BOOLEAN = { kind: "boolean" };
854
930
  const OBJECT = { kind: "object" };
855
- const REMOTE = {
856
- kind: "choice",
857
- choices: REMOTE_DEVICE_BACKENDS
858
- };
859
931
  const PARKED_MAX = {
860
932
  kind: "number",
861
933
  integer: true,
@@ -912,7 +984,7 @@ const SETTINGS = [
912
984
  default: "auto"
913
985
  },
914
986
  optimization("android.gradleBuildCache", "Gradle build cache"),
915
- optimization("android.targetAbiOnly", "Narrow Debug builds to the device ABI"),
987
+ optimization("android.targetAbiOnly", "Narrow Debug and hosted Android builds to the device ABI"),
916
988
  {
917
989
  key: "ios.deviceType",
918
990
  type: STRING,
@@ -939,8 +1011,8 @@ const SETTINGS = [
939
1011
  pattern: TAILNET_MACHINE_PATTERN,
940
1012
  patternHelp: "eas, proxy, auto, or a tailnet machine name"
941
1013
  },
942
- scopes: PROJECT,
943
- description: "Default iOS remote target: eas, proxy, or an approved Mac in hosting.machines; auto is not available yet"
1014
+ scopes: EVERY,
1015
+ description: "Default iOS remote target: eas, proxy, or an approved Mac in remote.machines; auto places on an approved Mac when this Mac is full or busy"
944
1016
  },
945
1017
  {
946
1018
  key: "ios.simslimProfile",
@@ -1032,9 +1104,13 @@ const SETTINGS = [
1032
1104
  },
1033
1105
  {
1034
1106
  key: "android.remote",
1035
- type: REMOTE,
1036
- scopes: PROJECT,
1037
- description: "Default remote backend for Android"
1107
+ type: {
1108
+ kind: "string",
1109
+ pattern: TAILNET_MACHINE_PATTERN,
1110
+ patternHelp: "eas, proxy, auto, or a tailnet machine name"
1111
+ },
1112
+ scopes: EVERY,
1113
+ description: "Default Android remote target: eas, proxy, or an approved Mac in remote.machines; auto places on an approved Mac when this Mac is full or busy"
1038
1114
  },
1039
1115
  {
1040
1116
  key: "metro.tunnel",
@@ -1101,9 +1177,20 @@ const SETTINGS = [
1101
1177
  minimum: 0
1102
1178
  },
1103
1179
  scopes: EVERY,
1104
- default: 0,
1180
+ default: 30,
1105
1181
  description: "Minutes an owned simulator or emulator stays idle before the workspace supervisor shuts it down; 0 never"
1106
1182
  },
1183
+ {
1184
+ key: "devices.reclaimIdleMinutes",
1185
+ type: {
1186
+ kind: "number",
1187
+ integer: true,
1188
+ minimum: 0
1189
+ },
1190
+ scopes: EVERY,
1191
+ default: 10,
1192
+ description: "Minutes an owned simulator or emulator stays idle before a waiting run reclaims its device slot; 0 never"
1193
+ },
1107
1194
  {
1108
1195
  key: "macos.product",
1109
1196
  type: {
@@ -1543,12 +1630,6 @@ const SETTINGS = [
1543
1630
  scopedHomeValue: 0,
1544
1631
  description: "Parked emulators kept for adoption; 0 turns parking off"
1545
1632
  },
1546
- {
1547
- key: "hosting.machines",
1548
- type: { kind: "strings" },
1549
- scopes: MACHINE,
1550
- description: "Tailscale names of the Macs that may host owned simulator sessions for this one, each optionally with :<port> of its tailscale serve route (default 7443); doctor --fix asks for separate device-host approval"
1551
- },
1552
1633
  {
1553
1634
  key: "hosting.agentDriver",
1554
1635
  type: {
@@ -1557,7 +1638,7 @@ const SETTINGS = [
1557
1638
  },
1558
1639
  scopes: MACHINE,
1559
1640
  default: "none",
1560
- description: "Tool this Mac starts so a client's coding agent can drive the macOS apps and iOS simulators it hosts for that client; none starts nothing"
1641
+ description: "Tool this Mac starts so a client's coding agent can drive the macOS apps, iOS simulators and Android emulators it hosts for that client; none starts nothing"
1561
1642
  },
1562
1643
  {
1563
1644
  key: "server.acceptClientBuilds",
@@ -1567,13 +1648,13 @@ const SETTINGS = [
1567
1648
  description: "Whether a Mac approved for builds or device hosting here may update this Mac's stim-server service to that Mac's own stim-server build; releases from npm need no setting"
1568
1649
  },
1569
1650
  {
1570
- key: "offload.machines",
1651
+ key: "remote.machines",
1571
1652
  type: { kind: "strings" },
1572
1653
  scopes: MACHINE,
1573
- description: "Tailscale names of the Macs that may build for this one, each optionally with :<port> of its tailscale serve route (default 7443)"
1654
+ description: "Tailscale names of the remote Macs this Mac may build on and host owned simulators on, each optionally with :<port> of its tailscale serve route (default 7443); what each is used for follows the build and device-host approvals it grants, which doctor --fix asks for separately"
1574
1655
  },
1575
1656
  {
1576
- key: "offload.machine",
1657
+ key: "remote.build",
1577
1658
  type: {
1578
1659
  kind: "string",
1579
1660
  pattern: TAILNET_MACHINE_PATTERN,
@@ -1581,38 +1662,38 @@ const SETTINGS = [
1581
1662
  },
1582
1663
  scopes: MACHINE,
1583
1664
  default: "auto",
1584
- env: "STIM_OFFLOAD_MACHINE",
1585
- description: "Build placement: auto follows offload.mode with local fallback; local always builds here; a name in offload.machines requires that machine without fallback. The ios, android and macos --build-machine flag overrides this setting and its environment override"
1665
+ env: "STIM_REMOTE_BUILD",
1666
+ description: "Build placement: auto follows remote.buildMode with local fallback; local always builds here; a name in remote.machines requires that remote Mac without fallback. The ios, android and macos --remote-build flag overrides this setting and its environment override"
1586
1667
  },
1587
1668
  {
1588
- key: "offload.mode",
1669
+ key: "remote.buildMode",
1589
1670
  type: {
1590
1671
  kind: "choice",
1591
1672
  choices: OFFLOAD_MODES
1592
1673
  },
1593
1674
  scopes: MACHINE,
1594
1675
  default: "auto",
1595
- env: "STIM_OFFLOAD_MODE",
1596
- description: "When offload.machine is auto, where iOS simulator Debug, Android emulator debug and macOS SwiftPM Debug builds run: auto builds here while this Mac has a free concurrency.maxBuilds slot and its load is under offload.maxLoadPerCore, and otherwise offloads to a less loaded machine in offload.machines; force offloads whenever a machine can take the build; off always builds here"
1676
+ env: "STIM_REMOTE_BUILD_MODE",
1677
+ description: "When remote.build is auto, where iOS simulator Debug, Android emulator debug and macOS SwiftPM Debug builds run: auto builds here while this Mac has a free concurrency.maxBuilds slot and its load is under server.maxLoadPerCore, and otherwise builds on a less loaded Mac in remote.machines; force builds on a remote Mac whenever one can take the build; off always builds here"
1597
1678
  },
1598
1679
  {
1599
- key: "offload.maxLoadPerCore",
1680
+ key: "server.maxLoadPerCore",
1600
1681
  type: {
1601
1682
  kind: "number",
1602
1683
  minimum: .1
1603
1684
  },
1604
1685
  scopes: MACHINE,
1605
1686
  default: 2,
1606
- description: "Load per core (5-minute load average divided by the CPU count) at which a Mac counts as saturated: a build machine declines offloaded builds, and auto offload stops preferring this Mac"
1687
+ description: "Load per core (5-minute load average divided by the CPU count) at which this Mac counts as saturated: it declines builds other Macs send it, automatic build placement stops preferring it, and automatic iOS and Android device placement treats it as busy"
1607
1688
  },
1608
1689
  {
1609
- key: "offload.workerRoot",
1690
+ key: "server.workerRoot",
1610
1691
  type: ABSOLUTE_PATH,
1611
1692
  scopes: MACHINE,
1612
- description: "Directory where stim-server keeps the checkouts, dependencies and build state of Macs that build here; default $STIM_HOME/build-worker"
1693
+ description: "Directory where stim-server keeps the checkouts, dependencies and build state of the Macs that build here; default $STIM_HOME/build-worker"
1613
1694
  },
1614
1695
  {
1615
- key: "offload.gradleDaemonIdleMinutes",
1696
+ key: "server.gradleDaemonIdleMinutes",
1616
1697
  type: {
1617
1698
  kind: "number",
1618
1699
  integer: true,
@@ -1621,7 +1702,7 @@ const SETTINGS = [
1621
1702
  },
1622
1703
  scopes: MACHINE,
1623
1704
  default: 30,
1624
- description: "Minutes the Gradle daemon of an offloaded Android build stays warm on this build machine after the build; 0 stops it when the build ends"
1705
+ description: "Minutes the Gradle daemon of an Android build another Mac sent here stays warm after the build; 0 stops it when the build ends"
1625
1706
  },
1626
1707
  {
1627
1708
  key: "caches.buildCache",
@@ -1791,7 +1872,7 @@ function tryAcquireBuildSlotClaim({ max, details }) {
1791
1872
  if (refusal && !busy) throw refusal;
1792
1873
  return null;
1793
1874
  }
1794
- /** Build slots held for a build machine's offloaded builds, which run under another Stim home. */
1875
+ /** Build slots held for a remote Mac's offloaded builds, which run under another Stim home. */
1795
1876
  function liveOffloadedSlots() {
1796
1877
  let names;
1797
1878
  try {
@@ -1809,9 +1890,9 @@ function liveOffloadedSlots() {
1809
1890
  return count;
1810
1891
  }
1811
1892
  function maxLoadPerCore() {
1812
- const value = loadConfig()?.offload?.maxLoadPerCore;
1893
+ const value = loadConfig()?.server?.maxLoadPerCore;
1813
1894
  if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
1814
- return settingDefinition("offload.maxLoadPerCore").default;
1895
+ return settingDefinition("server.maxLoadPerCore").default;
1815
1896
  }
1816
1897
  function liveNativeBuilds() {
1817
1898
  const dir = join(configDir(), "workspaces");
@@ -2594,7 +2675,7 @@ function listSegments(dir, closedOnly = false) {
2594
2675
  return segments.toSorted((a, b) => a.start - b.start);
2595
2676
  }
2596
2677
  /** Every device slot recorded under a workspace's recordings directory. */
2597
- function listRecordedDevices(recordingsDir) {
2678
+ function listRecordedDevices(recordingsDir, closedOnly = false) {
2598
2679
  return listDir(recordingsDir).flatMap((name) => {
2599
2680
  const match = DEVICE.exec(name);
2600
2681
  if (!match) return [];
@@ -2603,7 +2684,7 @@ function listRecordedDevices(recordingsDir) {
2603
2684
  platform: match[1],
2604
2685
  slot: match[2],
2605
2686
  dir,
2606
- segments: listSegments(dir)
2687
+ segments: listSegments(dir, closedOnly)
2607
2688
  }];
2608
2689
  });
2609
2690
  }
@@ -2840,6 +2921,8 @@ const WORKSPACE_PHASES = [
2840
2921
  "live",
2841
2922
  "idle"
2842
2923
  ];
2924
+ /** The tutorial format version the installed Stim writes into the tutorial app. */
2925
+ const TUTORIAL_VERSION = 1;
2843
2926
  /**
2844
2927
  * Where a memory figure comes from. `footprint` is each process's physical footprint, which Activity Monitor's Memory
2845
2928
  * column shows. `rss` sums resident set sizes, which count pages shared between processes once per process, so a
@@ -3131,7 +3214,7 @@ function readBuildDetail(root, claimId) {
3131
3214
  if (typeof raw.updatedAt !== "string") return null;
3132
3215
  return {
3133
3216
  step: NATIVE_BUILD_STEPS.includes(raw.step) ? raw.step : null,
3134
- unit: raw.unit === "targets" || raw.unit === "tasks" ? raw.unit : null,
3217
+ unit: raw.unit === "targets" || raw.unit === "tasks" || raw.unit === "steps" ? raw.unit : null,
3135
3218
  done: countOrNull(raw.done),
3136
3219
  total: countOrNull(raw.total),
3137
3220
  line: typeof raw.line === "string" ? raw.line : null,
@@ -3197,7 +3280,7 @@ function readAgentSessionsCache() {
3197
3280
  //#region state/stim-build.ts
3198
3281
  /**
3199
3282
  * A digest of Stim's built code: the first 16 hex characters of the sha256 over the sorted `*.mjs` names and
3200
- * bytes of a `stim` `dist` directory. A client and a build machine offload only when both compute the same one.
3283
+ * bytes of a `stim` `dist` directory. A client and a remote Mac offload only when both compute the same one.
3201
3284
  */
3202
3285
  function stimBuildDigest(dir) {
3203
3286
  try {
@@ -3225,6 +3308,7 @@ function readIdleStop(state) {
3225
3308
  idleMinutes: record.idleMinutes
3226
3309
  };
3227
3310
  }
3311
+ const DEVICE_RECLAIM_REASON = "reclaimed for a waiting run";
3228
3312
  const DEVICE_IDLE_SHUTDOWN_KEY = "deviceIdleShutdowns";
3229
3313
  /** The recorded idle shutdowns by device slot key (`ios`, `android`, `ios:<slot>`). */
3230
3314
  function readDeviceIdleShutdowns(state) {
@@ -3235,7 +3319,8 @@ function readDeviceIdleShutdowns(state) {
3235
3319
  const record = value;
3236
3320
  if (typeof record?.at === "string" && typeof record.idleMinutes === "number") found[key] = {
3237
3321
  at: record.at,
3238
- idleMinutes: record.idleMinutes
3322
+ idleMinutes: record.idleMinutes,
3323
+ ...typeof record.reason === "string" ? { reason: record.reason } : {}
3239
3324
  };
3240
3325
  }
3241
3326
  return found;
@@ -3354,6 +3439,22 @@ function buildDiagnostics(value) {
3354
3439
  }];
3355
3440
  }).toSorted((a, b) => Number(b.file !== null) - Number(a.file !== null)).slice(0, DIAGNOSTIC_CAP);
3356
3441
  }
3442
+ function devicePlacement(value) {
3443
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
3444
+ const record = value;
3445
+ if (record.decision !== "local" && record.decision !== "hosted" && record.decision !== "waited-locally" || typeof record.reason !== "string") return null;
3446
+ return {
3447
+ decision: record.decision,
3448
+ reason: record.reason,
3449
+ ...typeof record.machine === "string" ? { machine: record.machine } : {}
3450
+ };
3451
+ }
3452
+ function readDevicePlacement(state, platform, slot = "default") {
3453
+ const slots = isJsonObject(state?.deviceSlots) ? state.deviceSlots : {};
3454
+ const saved = isJsonObject(slots[slot]) ? slots[slot] : {};
3455
+ const device = slot === "default" ? state?.[platform] : saved[platform];
3456
+ return devicePlacement(isJsonObject(device) ? device.devicePlacement : void 0);
3457
+ }
3357
3458
  function lastBuildReport(platform, value) {
3358
3459
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
3359
3460
  const record = value;
@@ -3363,6 +3464,7 @@ function lastBuildReport(platform, value) {
3363
3464
  const finished = new Date(Date.parse(record.startedAt) + (durationMs ?? NaN));
3364
3465
  const reason = record.cacheHit === "local" || record.cacheHit === "remote" ? null : parseMissReason(record.missReason);
3365
3466
  const diagnostics = record.status === "failed" ? buildDiagnostics(record.diagnostics) : [];
3467
+ const placed = devicePlacement(record.devicePlacement);
3366
3468
  return {
3367
3469
  platform,
3368
3470
  status: record.status,
@@ -3374,6 +3476,7 @@ function lastBuildReport(platform, value) {
3374
3476
  finishedAt: Number.isNaN(finished.getTime()) ? null : finished.toISOString(),
3375
3477
  ...typeof record.errorCode === "string" ? { errorCode: record.errorCode } : {},
3376
3478
  ...reason ? { missReason: reason } : {},
3479
+ ...placed ? { devicePlacement: placed } : {},
3377
3480
  ...typeof record.buildMachine === "string" ? { buildMachine: record.buildMachine } : {},
3378
3481
  ...typeof record.builtOn === "string" ? { builtOn: record.builtOn } : {},
3379
3482
  ...typeof record.offloadedTo === "string" ? { offloadedTo: record.offloadedTo } : {},
@@ -3416,7 +3519,11 @@ function historyEntry(platform, value) {
3416
3519
  function readBuildHistory(state) {
3417
3520
  const history = state?.[BUILD_HISTORY_KEY];
3418
3521
  const builds = {};
3419
- for (const platform of ["ios", "android"]) {
3522
+ for (const platform of [
3523
+ "ios",
3524
+ "android",
3525
+ "macos"
3526
+ ]) {
3420
3527
  const list = history?.[platform];
3421
3528
  if (!Array.isArray(list)) continue;
3422
3529
  const entries = list.slice(0, 10).map((value) => historyEntry(platform, value)).filter((entry) => entry !== null);
@@ -3513,12 +3620,21 @@ function readViewedDevices(dir = deviceViewersDir()) {
3513
3620
  //#endregion
3514
3621
  //#region state/stats.ts
3515
3622
  const STATS_VERSION = 1;
3516
- /** Where a compiling build ran: here, on a build machine, or here after offloading it failed. */
3623
+ /** Where a compiling build ran: here, on a remote Mac, or here after offloading it failed. */
3517
3624
  const PLACEMENT_DECISIONS = [
3518
3625
  "here",
3519
3626
  "offloaded",
3520
3627
  "fell-back"
3521
3628
  ];
3629
+ const CAPACITY_REFUSAL_LIMIT = 50;
3630
+ const CAPACITY_REFUSAL_MAX_AGE_MS = 6048e5;
3631
+ function trimCapacityRefusals(events, now) {
3632
+ return events.filter((event) => {
3633
+ const age = now - Date.parse(event.at);
3634
+ return age >= 0 && age <= 6048e5;
3635
+ }).toSorted((a, b) => Date.parse(a.at) - Date.parse(b.at)).slice(-50);
3636
+ }
3637
+ const trimCapacityWaits = trimCapacityRefusals;
3522
3638
  const PLATFORMS = ["ios", "android"];
3523
3639
  const OUTCOMES = ["hit", "cold"];
3524
3640
  const HISTORY_LIMIT = 10;
@@ -3657,6 +3773,8 @@ function normalize(record) {
3657
3773
  if (Object.keys(normalized).length) history[key] = normalized;
3658
3774
  }
3659
3775
  const placements = Array.isArray(record.placements) ? record.placements.flatMap(normalizePlacement) : [];
3776
+ const capacityRefusals = Array.isArray(record.capacityRefusals) ? record.capacityRefusals.flatMap(normalizeCapacityRefusal) : [];
3777
+ const capacityWaits = Array.isArray(record.capacityWaits) ? record.capacityWaits.flatMap(normalizeCapacityWait) : [];
3660
3778
  const buildMachines = {};
3661
3779
  if (isObject(record.buildMachines)) {
3662
3780
  for (const [name, totals] of Object.entries(record.buildMachines)) if (isObject(totals)) buildMachines[name] = normalizeMachineTotals(totals);
@@ -3667,6 +3785,8 @@ function normalize(record) {
3667
3785
  projects,
3668
3786
  ...Object.keys(history).length ? { history } : {},
3669
3787
  ...placements.length ? { placements } : {},
3788
+ ...capacityRefusals.length ? { capacityRefusals } : {},
3789
+ ...capacityWaits.length ? { capacityWaits } : {},
3670
3790
  ...Object.keys(buildMachines).length ? { buildMachines } : {}
3671
3791
  };
3672
3792
  }
@@ -3677,6 +3797,7 @@ function normalizePlacement(value) {
3677
3797
  if (typeof reason !== "string" || typeof project !== "string") return [];
3678
3798
  const buildMs = wholeMs(value.buildMs);
3679
3799
  const slotWaitMs = wholeMs(value.slotWaitMs);
3800
+ const deviceSlotWaitMs = wholeMs(value.deviceSlotWaitMs);
3680
3801
  const localEstimateMs = wholeMs(value.localEstimateMs);
3681
3802
  return [{
3682
3803
  at: timestamp(value.at),
@@ -3687,10 +3808,40 @@ function normalizePlacement(value) {
3687
3808
  ...typeof machine === "string" && machine !== "" ? { machine } : {},
3688
3809
  ...buildMs > 0 ? { buildMs } : {},
3689
3810
  ...slotWaitMs > 0 ? { slotWaitMs } : {},
3811
+ ...deviceSlotWaitMs > 0 ? { deviceSlotWaitMs } : {},
3690
3812
  ...localEstimateMs > 0 ? { localEstimateMs } : {},
3691
3813
  ...value.failed === true ? { failed: true } : {}
3692
3814
  }];
3693
3815
  }
3816
+ function normalizeCapacityRefusal(value) {
3817
+ if (!isObject(value)) return [];
3818
+ const { at, kind, platform, max, workspace } = value;
3819
+ if (kind !== "device" || !PLATFORMS.includes(platform)) return [];
3820
+ if (typeof at !== "string" || !Number.isFinite(Date.parse(at))) return [];
3821
+ if (typeof max !== "number" || !Number.isInteger(max) || max <= 0) return [];
3822
+ if (typeof workspace !== "string" || workspace === "") return [];
3823
+ return [{
3824
+ at: new Date(at).toISOString(),
3825
+ kind,
3826
+ platform,
3827
+ max,
3828
+ workspace
3829
+ }];
3830
+ }
3831
+ function normalizeCapacityWait(value) {
3832
+ if (!isObject(value) || value.kind !== "device-wait") return [];
3833
+ const refusal = normalizeCapacityRefusal({
3834
+ ...value,
3835
+ kind: "device"
3836
+ })[0];
3837
+ if (!refusal || typeof value.ms !== "number" || !Number.isFinite(value.ms) || value.ms < 0) return [];
3838
+ return [{
3839
+ ...refusal,
3840
+ kind: "device-wait",
3841
+ ms: Math.round(value.ms),
3842
+ ...typeof value.reclaimed === "number" && Number.isInteger(value.reclaimed) && value.reclaimed > 0 ? { reclaimed: value.reclaimed } : {}
3843
+ }];
3844
+ }
3694
3845
  function normalizeMachineTotals(totals) {
3695
3846
  const savedMs = Number(totals.savedMs);
3696
3847
  return {
@@ -3810,6 +3961,8 @@ function decodeStats(text) {
3810
3961
  function readStatsReport(key, now) {
3811
3962
  const { record, note } = readStats();
3812
3963
  const project = key ? record?.projects[key] : null;
3964
+ const capacityRefusals = trimCapacityRefusals(record?.capacityRefusals ?? [], now).toReversed();
3965
+ const capacityWaits = trimCapacityWaits(record?.capacityWaits ?? [], now).toReversed();
3813
3966
  return {
3814
3967
  report: {
3815
3968
  version: 1,
@@ -3823,6 +3976,8 @@ function readStatsReport(key, now) {
3823
3976
  android: record?.machine.android ?? null
3824
3977
  },
3825
3978
  offload: offloadSummary(record, now),
3979
+ ...capacityRefusals.length ? { capacityRefusals } : {},
3980
+ ...capacityWaits.length ? { capacityWaits } : {},
3826
3981
  agentDevice: readAgentDeviceUsage(),
3827
3982
  swiftpmCache: readSwiftpmCacheUsage()
3828
3983
  },
@@ -3923,9 +4078,16 @@ function parseHostedOfferRequest(value) {
3923
4078
  ...typeof value.deviceProfile === "string" ? { deviceProfile: value.deviceProfile } : {}
3924
4079
  };
3925
4080
  }
4081
+ function parseHostedLocalDeviceCount(value) {
4082
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value;
4083
+ if (isJsonObject(value) && typeof value.unknown === "string" && value.unknown) return { unknown: value.unknown.slice(0, 4e3) };
4084
+ return null;
4085
+ }
3926
4086
  function parseHostedNativeOffer(value) {
3927
4087
  if (!isJsonObject(value) || !isJsonObject(value.resources)) return null;
3928
4088
  const { resources } = value;
4089
+ const local = parseHostedLocalDeviceCount(resources.localDevices);
4090
+ if (resources.localDevices !== void 0 && local === null) return null;
3929
4091
  if (value.platform !== "ios" && value.platform !== "android" && value.platform !== "macos" || value.declined !== null && (typeof value.declined !== "string" || !value.declined || value.declined.length > 4e3) || !Number.isInteger(resources.cpus) || Number(resources.cpus) < 1 || ["loadPerCore", "memoryFreeBytes"].some((key) => typeof resources[key] !== "number" || !Number.isFinite(resources[key]) || resources[key] < 0) || resources.workerDiskFreeBytes !== null && (typeof resources.workerDiskFreeBytes !== "number" || !Number.isFinite(resources.workerDiskFreeBytes) || resources.workerDiskFreeBytes < 0) || resources.memoryPressure !== null && resources.memoryPressure !== "normal" && resources.memoryPressure !== "warning" && resources.memoryPressure !== "critical") return null;
3930
4092
  let choice = null;
3931
4093
  if (value.choice !== null) {
@@ -3961,7 +4123,8 @@ function parseHostedNativeOffer(value) {
3961
4123
  loadPerCore: resources.loadPerCore,
3962
4124
  memoryFreeBytes: resources.memoryFreeBytes,
3963
4125
  memoryPressure: resources.memoryPressure,
3964
- workerDiskFreeBytes: resources.workerDiskFreeBytes
4126
+ workerDiskFreeBytes: resources.workerDiskFreeBytes,
4127
+ ...local !== null ? { localDevices: local } : {}
3965
4128
  }
3966
4129
  };
3967
4130
  }
@@ -3991,7 +4154,7 @@ function readHostedSessions() {
3991
4154
  "stopping",
3992
4155
  "stopped",
3993
4156
  "unknown"
3994
- ].includes(String(entry.state)) || entry.platform === "android" && !hostedConsolePort(entry.consolePort) || (entry.platform === "macos" ? !hostedMacosAppSlot(entry.appSlot) : entry.appSlot !== void 0) || entry.device !== null && !parseHostedPlatformDevice(entry.device, entry.platform) || entry.platform === "macos" && entry.device !== null && entry.device.appSlot !== entry.appSlot || entry.platform === "android" && entry.device !== null && entry.device.consolePort !== entry.consolePort || entry.state === "ready" && entry.device === null || entry.notice !== void 0 && typeof entry.notice !== "string" || entry.appAttempt !== void 0 && (typeof entry.appAttempt !== "string" || !/^[a-zA-Z0-9_-]{1,128}$/.test(entry.appAttempt)) || entry.metroPort !== void 0 && (typeof entry.metroPort !== "number" || !Number.isInteger(entry.metroPort) || entry.metroPort < 1 || entry.metroPort > 65535)) throw new Error(`Malformed hosted session record: ${root}`);
4157
+ ].includes(String(entry.state)) || entry.platform === "android" && !hostedConsolePort(entry.consolePort) || (entry.platform === "macos" ? !hostedMacosAppSlot(entry.appSlot) : entry.appSlot !== void 0) || entry.device !== null && !parseHostedPlatformDevice(entry.device, entry.platform) || entry.platform === "macos" && entry.device !== null && entry.device.appSlot !== entry.appSlot || entry.platform === "android" && entry.device !== null && entry.device.consolePort !== entry.consolePort || entry.state === "ready" && entry.device === null || entry.parked !== void 0 && (!isJsonObject(entry.parked) || typeof entry.parked.at !== "string" || !Number.isFinite(Date.parse(entry.parked.at)) || new Date(entry.parked.at).toISOString() !== entry.parked.at || entry.state !== "stopped" || entry.platform === "macos") || entry.notice !== void 0 && typeof entry.notice !== "string" || entry.appAttempt !== void 0 && (typeof entry.appAttempt !== "string" || !/^[a-zA-Z0-9_-]{1,128}$/.test(entry.appAttempt)) || entry.clientMetroPort !== void 0 && (typeof entry.clientMetroPort !== "number" || !Number.isInteger(entry.clientMetroPort) || entry.clientMetroPort < 1 || entry.clientMetroPort > 65535 || entry.platform !== "android") || entry.metroPort !== void 0 && (typeof entry.metroPort !== "number" || !Number.isInteger(entry.metroPort) || entry.metroPort < 1 || entry.metroPort > 65535)) throw new Error(`Malformed hosted session record: ${root}`);
3995
4158
  ids.add(entry.id);
3996
4159
  return entry;
3997
4160
  });
@@ -4270,27 +4433,45 @@ function macosAppState(record) {
4270
4433
  };
4271
4434
  }
4272
4435
  //#endregion
4273
- //#region state/hosted-ios.ts
4274
- function parseHostedIosPlacement(value) {
4436
+ //#region state/hosted-native.ts
4437
+ function parseHostedNativePlacement(value, platform) {
4275
4438
  if (!isJsonObject(value) || typeof value.machine !== "string" || !parseMachine(value.machine) || typeof value.session !== "string" || !/^[a-f0-9-]{36}$/.test(value.session)) return null;
4276
- const device = parseHostedDevice(value.device);
4277
4439
  return {
4278
4440
  machine: value.machine,
4279
4441
  session: value.session,
4280
4442
  appAttempt: typeof value.appAttempt === "string" && /^[a-zA-Z0-9_-]{1,128}$/.test(value.appAttempt) ? value.appAttempt : "",
4281
- selected: value.machine,
4282
- device,
4443
+ selected: typeof value.selected === "string" && parseMachine(value.selected) ? value.selected : value.machine,
4444
+ ...typeof value.reason === "string" ? { reason: value.reason } : {},
4445
+ device: parseHostedPlatformDevice(value.device, platform),
4283
4446
  agent: parseHostedAgentAccess(value.agent) ?? {
4284
4447
  driver: "none",
4285
4448
  setting: "hosting.agentDriver"
4286
4449
  }
4287
4450
  };
4288
4451
  }
4452
+ function hostedNativeRecords(state, platform) {
4453
+ const slots = isJsonObject(state?.deviceSlots) ? state.deviceSlots : {};
4454
+ const records = {
4455
+ default: state?.[platform],
4456
+ ...Object.fromEntries(Object.entries(slots).map(([slot, value]) => [slot, isJsonObject(value) ? value[platform] : void 0]))
4457
+ };
4458
+ return Object.fromEntries(Object.entries(records).flatMap(([slot, record]) => isJsonObject(record) && record.host !== void 0 ? [[slot, record.host]] : []));
4459
+ }
4460
+ function unreadableHostedNative(slot, platform) {
4461
+ const key = slot === "default" ? `${platform}.host` : `deviceSlots.${slot}.${platform}.host`;
4462
+ return `The hosted ${platform === "ios" ? "iOS" : "Android"} placement at workspace-state key ${key} is unreadable. Inspect ${key}.machine and ${key}.session before reconciling it.`;
4463
+ }
4464
+ //#endregion
4465
+ //#region state/hosted-ios.ts
4466
+ function parseHostedIosPlacement(value) {
4467
+ return parseHostedNativePlacement(value, "ios");
4468
+ }
4289
4469
  function hostedIosStatus(placement) {
4290
4470
  return {
4291
4471
  machine: placement.machine,
4292
4472
  session: placement.session,
4293
4473
  selected: placement.selected,
4474
+ ...placement.reason ? { reason: placement.reason } : {},
4294
4475
  agent: placement.agent,
4295
4476
  device: placement.device ? {
4296
4477
  name: placement.device.deviceType,
@@ -4298,18 +4479,11 @@ function hostedIosStatus(placement) {
4298
4479
  } : null
4299
4480
  };
4300
4481
  }
4301
- /** Includes unreadable placements so callers retain every recorded hosting slot. */
4302
4482
  function hostedIosRecords(state) {
4303
- const slots = isJsonObject(state?.deviceSlots) ? state.deviceSlots : {};
4304
- const records = {
4305
- default: state?.ios,
4306
- ...Object.fromEntries(Object.entries(slots).map(([slot, value]) => [slot, isJsonObject(value) ? value.ios : void 0]))
4307
- };
4308
- return Object.fromEntries(Object.entries(records).flatMap(([slot, record]) => isJsonObject(record) && record.host !== void 0 ? [[slot, record.host]] : []));
4483
+ return hostedNativeRecords(state, "ios");
4309
4484
  }
4310
4485
  function unreadableHostedIos(slot) {
4311
- const key = slot === "default" ? "ios.host" : `deviceSlots.${slot}.ios.host`;
4312
- return `The hosted iOS placement at workspace-state key ${key} is unreadable. Inspect ${key}.machine and ${key}.session before reconciling it.`;
4486
+ return unreadableHostedNative(slot, "ios");
4313
4487
  }
4314
4488
  function hostedIosPlacements(state) {
4315
4489
  return Object.assign(Object.create(null), Object.fromEntries(Object.entries(hostedIosRecords(state)).flatMap(([slot, record]) => {
@@ -4318,6 +4492,40 @@ function hostedIosPlacements(state) {
4318
4492
  })));
4319
4493
  }
4320
4494
  //#endregion
4495
+ //#region state/hosted-android.ts
4496
+ function parseHostedAndroidPlacement(value) {
4497
+ return parseHostedNativePlacement(value, "android");
4498
+ }
4499
+ function hostedAndroidStatus(placement) {
4500
+ return {
4501
+ machine: placement.machine,
4502
+ session: placement.session,
4503
+ selected: placement.selected,
4504
+ ...placement.reason ? { reason: placement.reason } : {},
4505
+ agent: placement.agent,
4506
+ device: placement.device ? {
4507
+ name: hostedAndroidDeviceName(placement.device),
4508
+ systemImage: placement.device.systemImage,
4509
+ api: Number(/^system-images;android-(\d+);/.exec(placement.device.systemImage)?.[1])
4510
+ } : null
4511
+ };
4512
+ }
4513
+ function hostedAndroidRecords(state) {
4514
+ return hostedNativeRecords(state, "android");
4515
+ }
4516
+ function unreadableHostedAndroid(slot) {
4517
+ return unreadableHostedNative(slot, "android");
4518
+ }
4519
+ function hostedAndroidPlacements(state) {
4520
+ return Object.assign(Object.create(null), Object.fromEntries(Object.entries(hostedAndroidRecords(state)).flatMap(([slot, record]) => {
4521
+ const placement = parseHostedAndroidPlacement(record);
4522
+ return placement ? [[slot, placement]] : [];
4523
+ })));
4524
+ }
4525
+ function hostedAndroidDeviceName(device) {
4526
+ return `${device.deviceProfile} (API ${/^system-images;android-(\d+);/.exec(device.systemImage)?.[1]})`;
4527
+ }
4528
+ //#endregion
4321
4529
  //#region state/hosted-metro.ts
4322
4530
  const HOSTED_METRO_REQUESTS_KEY = "hostedMetroRequests";
4323
4531
  const HOSTED_METRO_GATEWAYS_KEY = "hostedMetroGateways";
@@ -4360,15 +4568,19 @@ function hostedMacosLogsDir(home) {
4360
4568
  return null;
4361
4569
  }
4362
4570
  }
4363
- const hostedIosLogsDir = (home) => join(home, "ios-logs");
4571
+ const hostedNativeLogsDir = (home, platform) => join(home, `${platform}-logs`);
4364
4572
  /** The completed query window and overlap digests, or null for a missing, malformed or unreadable checkpoint. */
4365
- function readHostedIosLogsCheckpoint(home) {
4573
+ function readHostedNativeLogsCheckpoint(home, platform = "ios") {
4366
4574
  try {
4367
- const value = JSON.parse(readFileSync(join(hostedIosLogsDir(home), "checkpoint.json"), "utf8"));
4575
+ const value = JSON.parse(readFileSync(join(hostedNativeLogsDir(home, platform), "checkpoint.json"), "utf8"));
4368
4576
  if (!isJsonObject(value) || !Number.isSafeInteger(value.until) || value.until < 0 || value.windowMs !== void 0 && (!Number.isSafeInteger(value.windowMs) || value.windowMs < 1e3) || !Array.isArray(value.boundary) || !value.boundary.every((each) => typeof each === "string" && /^[a-f0-9]{64}$/.test(each))) return null;
4369
4577
  return {
4370
4578
  until: value.until,
4371
4579
  boundary: value.boundary,
4580
+ ...typeof value.appAttempt === "string" && Number.isSafeInteger(value.pid) && value.pid > 0 ? {
4581
+ appAttempt: value.appAttempt,
4582
+ pid: value.pid
4583
+ } : {},
4372
4584
  ...typeof value.windowMs === "number" ? { windowMs: value.windowMs } : {}
4373
4585
  };
4374
4586
  } catch {
@@ -4535,6 +4747,7 @@ function readArchive(id) {
4535
4747
  if (!id || id.startsWith(".") || basename(id) !== id || id.includes("\0")) return null;
4536
4748
  try {
4537
4749
  if (!lstatSync(join(archiveRoot(), id)).isDirectory()) return null;
4750
+ if (!lstatSync(join(archiveRoot(), id, "archive.json")).isFile()) return null;
4538
4751
  } catch {
4539
4752
  return null;
4540
4753
  }
@@ -4556,6 +4769,17 @@ function readArchive(id) {
4556
4769
  ].some((key) => typeof bytes[key] !== "number" || !Number.isFinite(bytes[key]) || bytes[key] < 0)) return null;
4557
4770
  return record;
4558
4771
  }
4772
+ function readArchiveState(id) {
4773
+ if (!id || id.startsWith(".") || basename(id) !== id || id.includes("\0")) return null;
4774
+ const file = join(archiveRoot(), id, "state.json");
4775
+ try {
4776
+ if (!lstatSync(join(archiveRoot(), id)).isDirectory()) return null;
4777
+ if (!lstatSync(file).isFile()) return null;
4778
+ } catch {
4779
+ return null;
4780
+ }
4781
+ return readJsonObject(file);
4782
+ }
4559
4783
  function archivedUsage(archives) {
4560
4784
  const usage = {
4561
4785
  count: archives.length,
@@ -4649,4 +4873,4 @@ function isSetupJournalPrunable(journal, now) {
4649
4873
  return Date.parse(journal.expiresAt) + 36e5 < now;
4650
4874
  }
4651
4875
  //#endregion
4652
- export { ACTIVITY_RECENCY_BASES, AGENT_TOOLS, ANDROID_COMPILER_CACHE_CHOICES, ANDROID_EMULATOR_APPS, ANDROID_PCH_CHOICES, APP_PROCESS_STATES, BUILD_HISTORY_KEY, BUILD_HISTORY_LIMIT, BUILD_PHASES, BUILD_RESULTS, DEVICE_IDLE_SHUTDOWN_KEY, DEV_SERVER_STOP_REQUEST_KEY, ENDED_AGENT_RETENTION_MS, ERROR_SOURCES, HISTORY_LIMIT, HOSTED_AGENT_DRIVERS, HOSTED_APP_CHUNK_BYTES, HOSTED_LOGS_FIRST_BYTES, HOSTED_LOGS_PAGE_BYTES, HOSTED_MACOS_APP_SLOTS, HOSTED_METRO_GATEWAYS_KEY, HOSTED_METRO_REQUESTS_KEY, IDLE_STOP_KEY, IOS_SIMULATOR_APPS, LAST_BUILD_KEYS, LEVELS, MACHINE_OWNER_KINDS, MEMORY_SOURCES, NATIVE_BUILD_STEPS, NATIVE_CRASH_LOG_PREFIX, OFFLOAD_MODES, PHYSICAL_DEVICE_CONNECTIONS, PLACEMENT_DECISIONS, READY_PHASE_MS, RECORDING_CAP_BYTES, RECORDING_FOOTAGE_MS, RECORDING_PLATFORMS, REMOTE_DEVICE_BACKENDS, SETTINGS, SETTINGS_SCHEMA_FILE, SETTINGS_SCHEMA_URL, SETTING_GROUPS, SETTING_SCOPES, SOURCES, STATS_VERSION, STATUS_ISSUE_CODES, TAILNET_MACHINE_PATTERN, TUNNEL_MODES, WARM_KEY, WEB_PAGE_STATES, WEB_VIEWPORTS, WORKSPACE_AGENT_KEY, WORKSPACE_PHASES, WORKSPACE_STAGE_KINDS, acceptsShape, advanceTail, agentDeviceStateDirs, agentDeviceUsageFile, agentSessionOf, agentSessionsCacheFile, allMetroPorts, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, assertHostedDeviceLedger, attachExpoErrorContext, buildCriteria, buildDiagnostics, buildMachinesFile, buildMachinesLock, buildSlotPath, buildSlotsDir, buildWorkerRoot, bundleMarkerPlatform, capDrops, closedSegmentName, coerceSettingText, compileGrep, configCorruptRepair, configLockPath, countErrorEntries, createAgentActionReader, createdDevicesFile, createdDevicesLock, decodeStats, deviceHostArea, deviceHostMachinesClaims, deviceHostMachinesFile, deviceHostMachinesLock, deviceHostRoot, deviceViewersDir, diskUsageCacheDir, diskUsageCacheFile, easMachineStateRoot, easSessionLedgerFile, easSessionLedgerLock, emulatorLogFile, endedAgentSessionOf, endpoint, expectedShape, fileSizes, findEnclosingWorktreeRoot, findPeer, findProjectByMetroPort, findProjectRoot, followLogs, footageDrops, getConcurrencyLimits, getConfigPath, getProject, getProjectSetting, getProjectSettings, getRepoSettings, gitMergeCacheDir, hostedAppArea, hostedAppAttempt, hostedAppBlobs, hostedConsolePort, hostedDeviceId, hostedIosLogsDir, hostedIosPlacements, hostedIosRecords, hostedIosStatus, hostedMacosAppSlot, hostedMacosBundleId, hostedMacosLogsDir, hostedMetroGateways, hostedMetroRequests, isJournalExpired, isJsonObject, isLayeredSetting, isPathPrefix, isSensitiveReference, isSetupJournalPrunable, lastUseFrom, levelRank, linkReplacedArchives, listRecordedDevices, listSegments, listStatusCacheEntries, loadConfig, logFiles, machineCapacity, macosAppState, maintenanceAttemptFile, maintenanceChildLogFile, maintenanceDir, maintenanceNdjsonFile, maintenanceRunClaims, maintenanceStateFile, markerWindow, offloadSummary, openSegmentName, parseHostedAgentAccess, parseHostedAgentGrant, parseHostedAndroidDevice, parseHostedAppManifest, parseHostedAppOffer, parseHostedChoice, parseHostedDevice, parseHostedIosPlacement, parseHostedLogsCursor, parseHostedMacosChoice, parseHostedMacosDevice, parseHostedMacosPlacement, parseHostedNativeOffer, parseHostedOfferRequest, parseHostedPlatformDevice, parseHostedRequest, parseMachine, parseMacosRecord, parseMaintenanceState, parseMissReason, parseNdjsonLine, parseNdjsonText, parseSetupJournal, parseSince, pinnedEndpoint, pullRequestCacheDir, pullRequestCacheFile, queryJsonLogs, queryLogs, readAgentDeviceUsage, readAgentDeviceUsageCache, readAgentSessionsCache, readArchive, readArchives, readBuildDetail, readBuildHistory, readBuildMachines, readCreatedDevices, readDevServerStop, readDevServerStopRequest, readDeviceHostMachines, readDeviceIdleShutdowns, readDiskUsage, readEasSessionLedger, readEndedAgentSessions, readHostedApp, readHostedAppMetadata, readHostedDevice, readHostedDeviceLedger, readHostedIosLogsCheckpoint, readHostedMacosApp, readHostedSessions, readIdleStop, readJsonFile, readJsonObject, readLastBuilds, readLogRecords, readLogTimeline, readLogsSince, readMacosRecord, readMaintenanceAttempt, readMaintenanceState, readNdjsonGenerations, readPullRequestCache, readRunEstimates, readSetupJournal, readStateFile, readStats, readStatsReport, readSwiftpmCacheUsage, readViewedDevices, readWarmRecord, readWorkspaceAgent, readWorkspaceState, recordMatches, recordedBytes, recordingDeviceName, recordingEnabled, rememberHostedAppBlobRename, saturation, serverSetupDir, settingDefinition, settingValueError, settingsJsonSchema, setupJournalFile, sharedCcache, sharedCompilationCache, sharedGradle, sharedPods, sortByTs, statsFile, statsProjectKey, stimBuildDigest, supervisorLogFile, supervisorPidFile, swiftpmCacheUsageFile, tailRead, trimSamples, tryAcquireBuildSlotClaim, unreadableHostedIos, validEasSessionClaim, validHostedAppArguments, validHostedAppBlob, wholePhases, withConfigLock, withDerivedFacts, withGitChip, withStateReadCache, workspaceAgentDeviceDir, workspaceBuildDetailFile, workspaceDerivedData, workspaceEndedAgentsFile, workspaceEndedAgentsLock, workspaceGradleBuild, workspaceLastUsed, workspaceLogErrorIndex, workspaceLogsDir, workspaceMetadataFile, workspaceRecordingsDir, workspaceStateFile, workspaceStateLock };
4876
+ export { ACTIVITY_RECENCY_BASES, AGENT_TOOLS, ANDROID_COMPILER_CACHE_CHOICES, ANDROID_EMULATOR_APPS, ANDROID_PCH_CHOICES, APP_PROCESS_STATES, BUILD_HISTORY_KEY, BUILD_HISTORY_LIMIT, BUILD_PHASES, BUILD_RESULTS, CAPACITY_REFUSAL_LIMIT, CAPACITY_REFUSAL_MAX_AGE_MS, DEVICE_IDLE_SHUTDOWN_KEY, DEVICE_RECLAIM_REASON, DEV_SERVER_STOP_REQUEST_KEY, ENDED_AGENT_RETENTION_MS, ERROR_SOURCES, HISTORY_LIMIT, HOSTED_AGENT_DRIVERS, HOSTED_APP_CHUNK_BYTES, HOSTED_LOGS_FIRST_BYTES, HOSTED_LOGS_PAGE_BYTES, HOSTED_MACOS_APP_SLOTS, HOSTED_METRO_GATEWAYS_KEY, HOSTED_METRO_REQUESTS_KEY, IDLE_STOP_KEY, IOS_SIMULATOR_APPS, LAST_BUILD_KEYS, LEVELS, MACHINE_OWNER_KINDS, MEMORY_SOURCES, NATIVE_BUILD_STEPS, NATIVE_CRASH_LOG_PREFIX, OFFLOAD_MODES, PHYSICAL_DEVICE_CONNECTIONS, PLACEMENT_DECISIONS, READY_PHASE_MS, RECORDING_CAP_BYTES, RECORDING_FOOTAGE_MS, RECORDING_PLATFORMS, REMOTE_DEVICE_BACKENDS, SETTINGS, SETTINGS_SCHEMA_FILE, SETTINGS_SCHEMA_URL, SETTING_GROUPS, SETTING_SCOPES, SOURCES, STATS_VERSION, STATUS_ISSUE_CODES, TAILNET_MACHINE_PATTERN, TUNNEL_MODES, TUTORIAL_VERSION, WARM_KEY, WEB_PAGE_STATES, WEB_VIEWPORTS, WORKSPACE_AGENT_KEY, WORKSPACE_PHASES, WORKSPACE_STAGE_KINDS, acceptsShape, advanceTail, agentDeviceStateDirs, agentDeviceUsageFile, agentSessionOf, agentSessionsCacheFile, allMetroPorts, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, assertHostedDeviceLedger, attachExpoErrorContext, buildCriteria, buildDiagnostics, buildMachinesFile, buildMachinesLock, buildSlotPath, buildSlotsDir, buildWorkerRoot, bundleMarkerPlatform, capDrops, closedSegmentName, coerceSettingText, compileGrep, configCorruptRepair, configLockPath, countErrorEntries, createAgentActionReader, createdDevicesFile, createdDevicesLock, decodeStats, deviceHostArea, deviceHostMachinesClaims, deviceHostMachinesFile, deviceHostMachinesLock, deviceHostRoot, deviceViewersDir, diskUsageCacheDir, diskUsageCacheFile, easMachineStateRoot, easSessionLedgerFile, easSessionLedgerLock, emulatorLogFile, endedAgentSessionOf, endpoint, expectedShape, fileSizes, findEnclosingWorktreeRoot, findPeer, findProjectByMetroPort, findProjectRoot, followLogs, footageDrops, getConcurrencyLimits, getConfigPath, getParkedMax, getProject, getProjectSetting, getProjectSettings, getRepoSettings, gitMergeCacheDir, hostedAndroidDeviceName, hostedAndroidPlacements, hostedAndroidRecords, hostedAndroidStatus, hostedAppArea, hostedAppAttempt, hostedAppBlobs, hostedConsolePort, hostedDeviceId, hostedIosPlacements, hostedIosRecords, hostedIosStatus, hostedMacosAppSlot, hostedMacosBundleId, hostedMacosLogsDir, hostedMetroGateways, hostedMetroRequests, hostedNativeLogsDir, hostedNativeRecords, isJournalExpired, isJsonObject, isLayeredSetting, isPathPrefix, isSensitiveReference, isSetupJournalPrunable, lastUseFrom, levelRank, linkReplacedArchives, listRecordedDevices, listSegments, listStatusCacheEntries, loadConfig, logFiles, machineCapacity, macosAppState, maintenanceAttemptFile, maintenanceChildLogFile, maintenanceDir, maintenanceNdjsonFile, maintenanceRunClaims, maintenanceStateFile, markerWindow, offloadSummary, openSegmentName, parseHostedAgentAccess, parseHostedAgentGrant, parseHostedAndroidDevice, parseHostedAndroidPlacement, parseHostedAppManifest, parseHostedAppOffer, parseHostedChoice, parseHostedDevice, parseHostedIosPlacement, parseHostedLocalDeviceCount, parseHostedLogsCursor, parseHostedMacosChoice, parseHostedMacosDevice, parseHostedMacosPlacement, parseHostedNativeOffer, parseHostedNativePlacement, parseHostedOfferRequest, parseHostedPlatformDevice, parseHostedRequest, parseMachine, parseMacosRecord, parseMaintenanceState, parseMissReason, parseNdjsonLine, parseNdjsonText, parseSetupJournal, parseSince, pinnedEndpoint, pullRequestCacheDir, pullRequestCacheFile, queryJsonLogs, queryLogs, readAgentDeviceUsage, readAgentDeviceUsageCache, readAgentSessionsCache, readArchive, readArchiveState, readArchives, readBuildDetail, readBuildHistory, readBuildMachines, readCreatedDevices, readDevServerStop, readDevServerStopRequest, readDeviceHostMachines, readDeviceIdleShutdowns, readDevicePlacement, readDiskUsage, readEasSessionLedger, readEndedAgentSessions, readHostedApp, readHostedAppMetadata, readHostedDevice, readHostedDeviceLedger, readHostedMacosApp, readHostedNativeLogsCheckpoint, readHostedSessions, readIdleStop, readJsonFile, readJsonObject, readLastBuilds, readLogRecords, readLogTimeline, readLogsSince, readMacosRecord, readMaintenanceAttempt, readMaintenanceState, readNdjsonGenerations, readPullRequestCache, readRunEstimates, readSetupJournal, readStateFile, readStats, readStatsReport, readSwiftpmCacheUsage, readViewedDevices, readWarmRecord, readWorkspaceAgent, readWorkspaceState, recordMatches, recordedBytes, recordingDeviceName, recordingEnabled, rememberHostedAppBlobRename, saturation, serverSetupDir, settingDefinition, settingValueError, settingsJsonSchema, setupJournalFile, sharedCcache, sharedCompilationCache, sharedGradle, sharedPods, sortByTs, statsFile, statsProjectKey, stimBuildDigest, supervisorLogFile, supervisorPidFile, swiftpmCacheUsageFile, tailRead, trimCapacityRefusals, trimCapacityWaits, trimSamples, tryAcquireBuildSlotClaim, unreadableHostedAndroid, unreadableHostedIos, unreadableHostedNative, validEasSessionClaim, validHostedAppArguments, validHostedAppBlob, wholePhases, withConfigLock, withDerivedFacts, withGitChip, withStateReadCache, workspaceAgentDeviceDir, workspaceBuildDetailFile, workspaceDerivedData, workspaceEndedAgentsFile, workspaceEndedAgentsLock, workspaceGradleBuild, workspaceLastUsed, workspaceLogErrorIndex, workspaceLogsDir, workspaceMetadataFile, workspaceRecordingsDir, workspaceStateFile, workspaceStateLock };