@stim-cli/core 1.20.2 → 1.21.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/dist/state.mjs CHANGED
@@ -1,16 +1,75 @@
1
- import { _ as releaseClaim, f as isClaimRefusal, g as readClaimSet, x as tryAcquireClaim } from "./ownership-claim-O31Y5iIb.mjs";
1
+ import { S as tryAcquireClaim, _ as releaseClaim, f as isClaimRefusal, g as readClaimSet } from "./ownership-claim-DHUs_Dir.mjs";
2
2
  import { inspectProcessIdentity } from "./process-identity.mjs";
3
- import { C as workspaceName, F as readJsonObject, I as readStateFile, L as withStateReadCache, N as isJsonObject, P as readJsonFile, R as withDirLock, T as workspaceStateDir, _ as rotatedLogPath, d as ensurePrivateDir, g as rotateLog, t as LOG_ROTATE_BYTES, u as configDir } from "./core-BkgfKqJ8.mjs";
4
- import { n as withGitChip, t as withDerivedFacts } from "./status-derived-C13g6eQt.mjs";
5
- import { appendFileSync, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync } from "node:fs";
3
+ import { C as workspaceName, G as withStateReadCache, H as readJsonFile, K as withDirLock, L as ambientContext, R as ambientEnv, T as workspaceStateDir, U as readJsonObject, V as isJsonObject, W as readStateFile, _ as rotatedLogPath, d as ensurePrivateDir, g as rotateLog, q as withDirLockAsync, t as LOG_ROTATE_BYTES, u as configDir } from "./core-DisO3hs1.mjs";
4
+ import { readOrCreateKeyFile } from "./noise-session.mjs";
5
+ import { validHomeId } from "./workspace-link.mjs";
6
+ import { a as workspaceSummary, i as statusViews, n as applyKeyedDelta, o as withDerivedFacts, r as SUMMARY_ARCHIVES, s as withGitChip, t as KeyedEntities } from "./keyed-delta-BBoOeR2J.mjs";
7
+ import { appendFileSync, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync, watch } from "node:fs";
6
8
  import { availableParallelism, homedir, loadavg } from "node:os";
7
- import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
9
+ import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
8
10
  import { createHash, randomBytes } from "node:crypto";
11
+ import { setImmediate as setImmediate$1 } from "node:timers/promises";
12
+ import { open, readFile, readdir, stat } from "node:fs/promises";
9
13
  import { isIP } from "node:net";
10
14
  import { StringDecoder } from "node:string_decoder";
15
+ //#region state/log-selection.ts
16
+ var BoundedSelection = class {
17
+ first;
18
+ compare;
19
+ tail;
20
+ constructor(compare, tail, first = false) {
21
+ this.first = first;
22
+ this.compare = compare;
23
+ this.tail = tail;
24
+ }
25
+ before(a, b) {
26
+ return (this.first ? this.compare(b, a) : this.compare(a, b)) < 0;
27
+ }
28
+ items = [];
29
+ get size() {
30
+ return this.items.length;
31
+ }
32
+ peek() {
33
+ return this.items[0];
34
+ }
35
+ keep(entry) {
36
+ if (this.tail !== void 0 && this.tail <= 0) return;
37
+ const items = this.items;
38
+ items.push(entry);
39
+ let i = items.length - 1;
40
+ while (i > 0) {
41
+ const parent = i - 1 >> 1;
42
+ if (!this.before(items[i], items[parent])) break;
43
+ [items[i], items[parent]] = [items[parent], items[i]];
44
+ i = parent;
45
+ }
46
+ if (this.tail !== void 0 && items.length > this.tail) this.pop();
47
+ }
48
+ sorted() {
49
+ return this.items.toSorted(this.compare);
50
+ }
51
+ pop() {
52
+ const items = this.items;
53
+ const last = items.pop();
54
+ if (!items.length) return;
55
+ items[0] = last;
56
+ let i = 0;
57
+ for (;;) {
58
+ const left = 2 * i + 1;
59
+ const right = left + 1;
60
+ let least = i;
61
+ if (left < items.length && this.before(items[left], items[least])) least = left;
62
+ if (right < items.length && this.before(items[right], items[least])) least = right;
63
+ if (least === i) return;
64
+ [items[i], items[least]] = [items[least], items[i]];
65
+ i = least;
66
+ }
67
+ }
68
+ };
69
+ //#endregion
11
70
  //#region state/agent-actions.ts
12
71
  const EVENTS_VERSION = 1;
13
- function object$1(value) {
72
+ function object$2(value) {
14
73
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
15
74
  }
16
75
  function parseAgentEvents(lines) {
@@ -22,7 +81,7 @@ function parseAgentEvents(lines) {
22
81
  for (const line of lines) {
23
82
  let entry;
24
83
  try {
25
- entry = object$1(JSON.parse(line));
84
+ entry = object$2(JSON.parse(line));
26
85
  } catch {
27
86
  continue;
28
87
  }
@@ -55,7 +114,7 @@ function parseAgentEvents(lines) {
55
114
  command,
56
115
  summary: typeof summary === "string" ? summary : "",
57
116
  failed,
58
- details: object$1(entry.details),
117
+ details: object$2(entry.details),
59
118
  requestId
60
119
  });
61
120
  }
@@ -173,41 +232,62 @@ function readCompleteLines(path, start) {
173
232
  }
174
233
  }
175
234
  function trackMacosOpen(cursor, event, unknownVersion) {
176
- const flags = object$1(event.details?.flags);
235
+ const flags = object$2(event.details?.flags);
177
236
  if (!event.failed) cursor.macos = event.details?.platform === "macos" || flags?.platform === "macos";
178
237
  cursor.bundleId = cursor.macos && !event.failed && !unknownVersion && flags?.surface === "app" && typeof event.details?.appBundleId === "string" ? event.details.appBundleId : null;
179
238
  }
239
+ function agentRecordCollector(selection) {
240
+ const out = [];
241
+ const bounded = selection?.tail === void 0 ? null : new BoundedSelection((a, b) => Number(a.record.ts) - Number(b.record.ts) || a.position - b.position, selection.tail);
242
+ let position = 0;
243
+ const keep = (record) => {
244
+ if (selection && !selection.matches(record)) return;
245
+ if (bounded) bounded.keep({
246
+ record,
247
+ position: position++
248
+ });
249
+ else out.push(record);
250
+ };
251
+ return {
252
+ keep,
253
+ records: () => bounded ? bounded.sorted().map(({ record }) => record) : out.toSorted((a, b) => a.ts - b.ts)
254
+ };
255
+ }
180
256
  /**
181
257
  * Reads agent-device session actions incrementally from the supplied sessions directories. With targets,
182
258
  * records are restricted to those devices; without targets, retained session metadata supplies attribution.
183
259
  * Missing directories and unreadable files yield no records.
184
260
  */
185
- function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceTs = -Infinity, now = Date.now, claimedDevice = () => null }) {
261
+ function agentActionReads({ sessionsDirs, targets: resolveTargets, sinceTs = -Infinity, now = Date.now, claimedDevice = () => null }) {
186
262
  const cursors = /* @__PURE__ */ new Map();
187
- return () => {
263
+ return function* (selection) {
188
264
  const targets = typeof resolveTargets === "function" ? resolveTargets() : resolveTargets;
189
265
  if (targets && !targets.length) return [];
190
266
  const byId = new Map(targets?.map((target) => [target.id, target]));
191
267
  const macosByBundle = new Map(targets?.flatMap((target) => target.platform === "macos" ? [[target.bundleId, target]] : []));
192
- const sessionDirs = (typeof sessionsDirs === "function" ? sessionsDirs() : sessionsDirs).flatMap((sessionsDir) => {
193
- try {
194
- return readdirSync(sessionsDir).map((name) => ({
195
- name,
196
- sessionsDir,
197
- dir: join(sessionsDir, name)
198
- }));
199
- } catch {
200
- return [];
201
- }
202
- });
203
- const out = [];
268
+ const sessionDirs = [];
269
+ for (const sessionsDir of typeof sessionsDirs === "function" ? sessionsDirs() : sessionsDirs) try {
270
+ const names = yield {
271
+ kind: "names",
272
+ path: sessionsDir
273
+ };
274
+ sessionDirs.push(...names.map((name) => ({
275
+ name,
276
+ sessionsDir,
277
+ dir: join(sessionsDir, name)
278
+ })));
279
+ } catch {}
280
+ const { keep, records } = agentRecordCollector(selection);
204
281
  for (const { name, sessionsDir, dir } of sessionDirs) {
205
282
  const eventsPath = join(dir, "events.ndjson");
206
283
  const lines = [];
207
284
  let cursor = cursors.get(dir);
208
285
  if (!cursor) {
209
286
  try {
210
- if (statSync(eventsPath).mtimeMs < sinceTs) continue;
287
+ if ((yield {
288
+ kind: "stat",
289
+ path: eventsPath
290
+ }).mtimeMs < sinceTs) continue;
211
291
  } catch {
212
292
  continue;
213
293
  }
@@ -224,19 +304,45 @@ function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceT
224
304
  };
225
305
  cursors.set(dir, cursor);
226
306
  try {
227
- lines.push(...readFileSync(`${eventsPath}.1`, "utf8").split("\n"));
307
+ lines.push(...(yield {
308
+ kind: "text",
309
+ path: `${eventsPath}.1`
310
+ }).split("\n"));
228
311
  } catch {}
229
312
  }
230
- let chunk = readCompleteLines(eventsPath, cursor.offset);
313
+ let chunk = yield {
314
+ kind: "lines",
315
+ path: eventsPath,
316
+ start: cursor.offset
317
+ };
231
318
  if (chunk && (chunk.size < cursor.offset || cursor.identity && chunk.identity !== cursor.identity)) {
232
319
  cursor.bundleId = null;
233
- chunk = readCompleteLines(eventsPath, 0);
320
+ chunk = yield {
321
+ kind: "lines",
322
+ path: eventsPath,
323
+ start: 0
324
+ };
234
325
  }
235
326
  if (!chunk) continue;
236
327
  cursor.identity = chunk.identity;
237
328
  cursor.offset = chunk.next;
238
329
  lines.push(...chunk.text.split("\n"));
239
- const parsed = parseAgentEvents(lines);
330
+ const parsed = {
331
+ events: [],
332
+ started: [],
333
+ finished: [],
334
+ session: null,
335
+ unknownVersion: null
336
+ };
337
+ for (let offset = 0; offset < lines.length; offset += 500) {
338
+ const batch = parseAgentEvents(lines.slice(offset, offset + 500));
339
+ parsed.events.push(...batch.events);
340
+ parsed.started.push(...batch.started);
341
+ parsed.finished.push(...batch.finished);
342
+ parsed.session ??= batch.session;
343
+ parsed.unknownVersion ??= batch.unknownVersion;
344
+ yield { kind: "turn" };
345
+ }
240
346
  for (const [requestId, at] of parsed.started) cursor.started.set(requestId, at);
241
347
  cursor.session ??= parsed.session;
242
348
  const session = cursor.session;
@@ -248,12 +354,18 @@ function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceT
248
354
  const runnerPath = join(dir, "runner.log");
249
355
  let runnerSize = -1;
250
356
  try {
251
- runnerSize = statSync(runnerPath).size;
357
+ runnerSize = (yield {
358
+ kind: "stat",
359
+ path: runnerPath
360
+ }).size;
252
361
  } catch {}
253
362
  if (cursor.runner?.size !== runnerSize) {
254
363
  let text = "";
255
364
  try {
256
- text = readFileSync(runnerPath, "utf8");
365
+ text = yield {
366
+ kind: "text",
367
+ path: runnerPath
368
+ };
257
369
  } catch {}
258
370
  cursor.runner = {
259
371
  size: runnerSize,
@@ -274,11 +386,12 @@ function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceT
274
386
  const target = !targets ? retainedTarget() : cursor.macos ? cursor.bundleId ? macosByBundle.get(cursor.bundleId) : void 0 : deviceId ? byId.get(deviceId) : void 0;
275
387
  if (target) {
276
388
  cursor.unknownReported = true;
277
- out.push(formatUnknownRecord(session ?? name, parsed.unknownVersion, target, now()));
389
+ keep(formatUnknownRecord(session ?? name, parsed.unknownVersion, target, now()));
278
390
  }
279
391
  }
280
392
  if (parsed.unknownVersion) cursor.bundleId = null;
281
- for (const event of parsed.events) {
393
+ for (const [index, event] of parsed.events.entries()) {
394
+ if (index % 500 === 0) yield { kind: "turn" };
282
395
  if (event.kind === "request.started") {
283
396
  cursor.closing = event.command === "close" ? cursor.bundleId : null;
284
397
  cursor.bundleId = null;
@@ -291,7 +404,7 @@ function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceT
291
404
  cursor.closing = null;
292
405
  const target = bundleId ? macosByBundle.get(bundleId) : void 0;
293
406
  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));
407
+ if (target?.platform === "macos" && event.ts >= Math.max(sinceTs, target.launchedAt) && (startedAt ?? event.ts) >= target.launchedAt) keep(agentRecord(event, target.id, target, startedAt));
295
408
  if (event.command === "close") cursor.bundleId = null;
296
409
  continue;
297
410
  }
@@ -300,10 +413,99 @@ function createAgentActionReader({ sessionsDirs, targets: resolveTargets, sinceT
300
413
  const deviceId = !targets && cursor.platform === "android" ? null : deviceAt(event.ts);
301
414
  const target = targets ? deviceId ? byId.get(deviceId) : void 0 : retainedTarget();
302
415
  const startedAt = event.requestId ? matched.get(event.requestId) : void 0;
303
- if (target) out.push(agentRecord(event, deviceId || null, target, startedAt));
416
+ if (target) keep(agentRecord(event, deviceId || null, target, startedAt));
304
417
  }
305
418
  }
306
- return out.toSorted((a, b) => a.ts - b.ts);
419
+ return records();
420
+ };
421
+ }
422
+ function syncRead(request) {
423
+ switch (request.kind) {
424
+ case "names": return readdirSync(request.path);
425
+ case "stat": return statSync(request.path);
426
+ case "text": return readFileSync(request.path, "utf8");
427
+ case "lines": return readCompleteLines(request.path, request.start);
428
+ case "turn": return;
429
+ }
430
+ }
431
+ /** Reads new agent-device actions synchronously, retaining attribution cursors between reads. */
432
+ function createAgentActionReader(options) {
433
+ const reads = agentActionReads(options);
434
+ return () => {
435
+ const iterator = reads();
436
+ let step = iterator.next();
437
+ while (!step.done) {
438
+ let value;
439
+ try {
440
+ value = syncRead(step.value);
441
+ } catch (error) {
442
+ step = iterator.throw(error);
443
+ continue;
444
+ }
445
+ step = iterator.next(value);
446
+ }
447
+ return step.value;
448
+ };
449
+ }
450
+ async function asyncRead(request) {
451
+ switch (request.kind) {
452
+ case "names": return readdir(request.path);
453
+ case "stat": return stat(request.path);
454
+ case "text": return readFile(request.path, "utf8");
455
+ case "turn": return setImmediate$1();
456
+ case "lines": {
457
+ let handle;
458
+ try {
459
+ handle = await open(request.path, "r");
460
+ } catch {
461
+ return null;
462
+ }
463
+ try {
464
+ const stats = await handle.stat({ bigint: true });
465
+ const size = Number(stats.size);
466
+ const identity = `${stats.dev}:${stats.ino}`;
467
+ if (size <= request.start) return {
468
+ text: "",
469
+ next: request.start,
470
+ size,
471
+ identity
472
+ };
473
+ const buffer = Buffer.alloc(size - request.start);
474
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, request.start);
475
+ const end = buffer.subarray(0, bytesRead).lastIndexOf(10);
476
+ return {
477
+ text: end < 0 ? "" : buffer.toString("utf8", 0, end + 1),
478
+ next: request.start + end + 1,
479
+ size,
480
+ identity
481
+ };
482
+ } catch {
483
+ return null;
484
+ } finally {
485
+ await handle.close();
486
+ }
487
+ }
488
+ }
489
+ }
490
+ /** Reads new agent-device actions asynchronously; selection applies only to matching output from this call. */
491
+ function createAsyncAgentActionReader(options, signal) {
492
+ const reads = agentActionReads(options);
493
+ return async (selection) => {
494
+ signal?.throwIfAborted();
495
+ const iterator = reads(selection);
496
+ let step = iterator.next();
497
+ while (!step.done) {
498
+ let value;
499
+ try {
500
+ value = await asyncRead(step.value);
501
+ } catch (error) {
502
+ step = iterator.throw(error);
503
+ continue;
504
+ }
505
+ signal?.throwIfAborted();
506
+ step = iterator.next(value);
507
+ }
508
+ return step.value;
307
509
  };
308
510
  }
309
511
  //#endregion
@@ -315,7 +517,7 @@ function readAgentDeviceUsageCache() {
315
517
  const value = readJsonObject(agentDeviceUsageFile());
316
518
  if (!value || value.version !== 1) return null;
317
519
  const { roots, ...usage } = value;
318
- if (!validUsage(usage) || !validRoots(roots)) return null;
520
+ if (!validUsage$1(usage) || !validRoots(roots)) return null;
319
521
  return {
320
522
  usage,
321
523
  roots
@@ -325,34 +527,34 @@ function readAgentDeviceUsage() {
325
527
  const value = readJsonObject(agentDeviceUsageFile());
326
528
  if (!value) return null;
327
529
  const { roots: _roots, ...usage } = value;
328
- return validUsage(usage) ? usage : null;
530
+ return validUsage$1(usage) ? usage : null;
329
531
  }
330
- function object(value) {
532
+ function object$1(value) {
331
533
  return value !== null && typeof value === "object" && !Array.isArray(value);
332
534
  }
333
- function bytes(value) {
535
+ function bytes$1(value) {
334
536
  return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
335
537
  }
336
538
  function sized(value) {
337
- return object(value) && typeof value.dir === "string" && bytes(value.bytes);
539
+ return object$1(value) && typeof value.dir === "string" && bytes$1(value.bytes);
338
540
  }
339
541
  function count$1(value) {
340
542
  return typeof value === "number" && Number.isInteger(value) && value >= 0;
341
543
  }
342
- function nullableString(value) {
544
+ function nullableString$1(value) {
343
545
  return value === null || typeof value === "string";
344
546
  }
345
547
  function validRoots(value) {
346
- return object(value) && typeof value.stateDir === "string" && typeof value.runnerRoot === "string" && typeof value.hostedDir === "string" && Array.isArray(value.workspaceDirs) && value.workspaceDirs.every((dir) => typeof dir === "string");
548
+ return object$1(value) && typeof value.stateDir === "string" && typeof value.runnerRoot === "string" && typeof value.hostedDir === "string" && Array.isArray(value.workspaceDirs) && value.workspaceDirs.every((dir) => typeof dir === "string");
347
549
  }
348
- function validUsage(value) {
550
+ function validUsage$1(value) {
349
551
  const { stateDir: state, runnerBuilds: runner, workspaces, hosted } = value;
350
- return value.version === 1 && typeof value.measuredAt === "string" && Number.isFinite(Date.parse(value.measuredAt)) && typeof value.bytes === "number" && bytes(value.bytes) && typeof value.complete === "boolean" && sized(state) && typeof state.present === "boolean" && sized(state.sessions) && count$1(state.sessions.count) && sized(state.logs) && object(state.other) && bytes(state.other.bytes) && Array.isArray(state.other.largest) && state.other.largest.every((entry) => object(entry) && typeof entry.name === "string" && bytes(entry.bytes)) && sized(runner) && typeof runner.present === "boolean" && bytes(runner.sharedBytes) && Array.isArray(runner.platforms) && runner.platforms.every((platform) => sized(platform) && typeof platform.platform === "string" && Array.isArray(platform.entries) && platform.entries.every((entry) => sized(entry) && typeof entry.name === "string" && nullableString(entry.lastUsedAt) && nullableString(entry.packageVersion) && nullableString(entry.xcodeBuildVersion) && typeof entry.inUse === "boolean" && [
552
+ return value.version === 1 && typeof value.measuredAt === "string" && Number.isFinite(Date.parse(value.measuredAt)) && typeof value.bytes === "number" && bytes$1(value.bytes) && typeof value.complete === "boolean" && sized(state) && typeof state.present === "boolean" && sized(state.sessions) && count$1(state.sessions.count) && sized(state.logs) && object$1(state.other) && bytes$1(state.other.bytes) && Array.isArray(state.other.largest) && state.other.largest.every((entry) => object$1(entry) && typeof entry.name === "string" && bytes$1(entry.bytes)) && sized(runner) && typeof runner.present === "boolean" && bytes$1(runner.sharedBytes) && Array.isArray(runner.platforms) && runner.platforms.every((platform) => sized(platform) && typeof platform.platform === "string" && Array.isArray(platform.entries) && platform.entries.every((entry) => sized(entry) && typeof entry.name === "string" && nullableString$1(entry.lastUsedAt) && nullableString$1(entry.packageVersion) && nullableString$1(entry.xcodeBuildVersion) && typeof entry.inUse === "boolean" && [
351
553
  null,
352
554
  "lease",
353
555
  "lock",
354
556
  "unreadable"
355
- ].includes(entry.inUseReason))) && Array.isArray(workspaces) && workspaces.every((entry) => sized(entry) && nullableString(entry.projectRoot)) && (hosted === null || sized(hosted) && count$1(hosted.sessions));
557
+ ].includes(entry.inUseReason))) && Array.isArray(workspaces) && workspaces.every((entry) => sized(entry) && nullableString$1(entry.projectRoot)) && (hosted === null || sized(hosted) && count$1(hosted.sessions));
356
558
  }
357
559
  //#endregion
358
560
  //#region state/swiftpm-cache-usage.ts
@@ -374,6 +576,30 @@ function readSwiftpmCacheUsage() {
374
576
  };
375
577
  }
376
578
  //#endregion
579
+ //#region state/machine-cache-usage.ts
580
+ function machineCacheUsageFile() {
581
+ return join(configDir(), "machine-cache-usage.json");
582
+ }
583
+ function object(value) {
584
+ return value !== null && typeof value === "object" && !Array.isArray(value);
585
+ }
586
+ function bytes(value) {
587
+ return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
588
+ }
589
+ function nullableString(value) {
590
+ return value === null || typeof value === "string";
591
+ }
592
+ function validRuntime(value) {
593
+ return object(value) && typeof value.identifier === "string" && typeof value.runtimeIdentifier === "string" && nullableString(value.version) && nullableString(value.build) && bytes(value.bytes) && typeof value.deletable === "boolean" && nullableString(value.lastUsedAt) && typeof value.devices === "number" && Number.isInteger(value.devices) && value.devices >= 0;
594
+ }
595
+ function validUsage(value) {
596
+ return Boolean(value && value.version === 1 && typeof value.measuredAt === "string" && Number.isFinite(Date.parse(value.measuredAt)) && Array.isArray(value.roots) && value.roots.every((root) => typeof root === "string") && Array.isArray(value.sizes) && value.sizes.every((entry) => object(entry) && typeof entry.dir === "string" && bytes(entry.bytes)) && (value.runtimes === null || Array.isArray(value.runtimes) && value.runtimes.every(validRuntime)) && nullableString(value.agentDeviceVersion) && nullableString(value.xcodeBuildVersion));
597
+ }
598
+ function readMachineCacheUsage() {
599
+ const value = readJsonObject(machineCacheUsageFile());
600
+ return validUsage(value) ? value : null;
601
+ }
602
+ //#endregion
377
603
  //#region state/paths.ts
378
604
  function getConfigPath() {
379
605
  return join(configDir(), "config.json");
@@ -459,14 +685,14 @@ function diskUsageCacheDir() {
459
685
  function diskUsageWalkClaims() {
460
686
  return join(configDir(), "disk-usage-walk.claims");
461
687
  }
462
- /** The remote Macs this Mac paired with, each with its pinned tailnet node and device token. */
688
+ /** The remote machines this machine asked for build access, each with its pinned stim-server key once approved. */
463
689
  function buildMachinesFile() {
464
690
  return join(configDir(), "build-machines.json");
465
691
  }
466
692
  function buildMachinesLock() {
467
693
  return join(configDir(), "build-machines.lock");
468
694
  }
469
- /** The remote Macs this Mac asked for access, separate from build credentials. */
695
+ /** The remote machines this machine asked for access, separate from build credentials. */
470
696
  function deviceHostMachinesFile() {
471
697
  return join(configDir(), "device-host-machines.json");
472
698
  }
@@ -523,6 +749,14 @@ function archiveDir(id) {
523
749
  function archiveLock() {
524
750
  return join(archiveRoot(), ".lock");
525
751
  }
752
+ /** This machine's own long-term X25519 key as a client of other machines' stim-servers. */
753
+ function clientIdentityKeyFile() {
754
+ return join(configDir(), "client-identity.key");
755
+ }
756
+ /** The Unix socket stim-server serves this machine's own user on, without Noise or pairing. */
757
+ function serverLocalSocket() {
758
+ return join(configDir(), "server", "local.sock");
759
+ }
526
760
  function serverSetupDir() {
527
761
  return join(configDir(), "server", "setup");
528
762
  }
@@ -534,6 +768,13 @@ function setupJournalFile(hash) {
534
768
  function withConfigLock(fn) {
535
769
  return withDirLock(configLockPath(), fn, { ensureParent: () => ensurePrivateDir(configDir()) });
536
770
  }
771
+ /** Waits for the config lock without blocking the event loop. The signal cancels acquisition; `fn` runs synchronously. */
772
+ function withConfigLockAsync(fn, signal) {
773
+ return withDirLockAsync(configLockPath(), fn, {
774
+ ensureParent: () => ensurePrivateDir(configDir()),
775
+ signal
776
+ });
777
+ }
537
778
  function configCorruptRepair(path = getConfigPath()) {
538
779
  return `Repair the file, or move it aside to start over: mv "${path}" "${path}.broken"`;
539
780
  }
@@ -546,7 +787,21 @@ function configCorrupt(reason, path = getConfigPath()) {
546
787
  /** The build worker root: `server.workerRoot` when it is absolute, else `$STIM_HOME/build-worker`. */
547
788
  function buildWorkerRoot(config = loadConfig()) {
548
789
  const configured = config?.server?.workerRoot;
549
- return typeof configured === "string" && configured.startsWith("/") ? configured : join(configDir(), "build-worker");
790
+ return typeof configured === "string" && isAbsolute(configured) ? configured : join(configDir(), "build-worker");
791
+ }
792
+ /**
793
+ * A short directory at the drive root that stands in for the build worker directory `path` on Windows. The NDK's
794
+ * ninja is not long-path aware (STIM_PATH_TOO_LONG), and it opens paths under the checkout and under the Gradle user
795
+ * home's transforms cache, both of which are too long under the worker root. A subst drive does not help: Node's
796
+ * native realpath, which expo-modules-autolinking uses, resolves it back to the long path.
797
+ */
798
+ function windowsShortDir(path, systemDrive = process.env.SystemDrive ?? "C:") {
799
+ const id = createHash("sha256").update(path.toLowerCase()).digest("hex").slice(0, 6);
800
+ return win32.join(`${systemDrive}\\`, "sw", id);
801
+ }
802
+ /** The ASCII key a Windows build worker records beside a `windowsShortDir` to tell which `kind` and `path` it serves. */
803
+ function windowsShortDirKey(kind, path) {
804
+ return createHash("sha256").update(`${kind}:${path.toLowerCase()}`).digest("hex");
550
805
  }
551
806
  function loadConfig() {
552
807
  const p = getConfigPath();
@@ -589,7 +844,7 @@ function registryFromJson(config, key, path) {
589
844
  function getProject(projectPath) {
590
845
  return loadConfig()?.projects?.[projectPath] || null;
591
846
  }
592
- function getConcurrencyLimits({ env = process.env } = {}) {
847
+ function getConcurrencyLimits({ env = ambientEnv() } = {}) {
593
848
  const c = loadConfig()?.concurrency || {};
594
849
  return {
595
850
  maxBuilds: resolveLimit(env.STIM_MAX_BUILDS, c.maxBuilds),
@@ -611,7 +866,7 @@ function parseMax(raw, strings) {
611
866
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return null;
612
867
  return value;
613
868
  }
614
- function getParkedMax(platform, { config, env = process.env } = {}) {
869
+ function getParkedMax(platform, { config, env = ambientEnv() } = {}) {
615
870
  const { key, env: envKey } = MAX_SETTING[platform];
616
871
  const fromEnv = env[envKey];
617
872
  const explicit = fromEnv !== void 0 && fromEnv !== "";
@@ -644,22 +899,9 @@ function resolveLimit(envVal, cfgVal) {
644
899
  function getProjectSettings(projectPath) {
645
900
  return getProject(projectPath)?.settings || {};
646
901
  }
647
- function getProjectSetting(projectPath, dottedKey) {
648
- return readNested(getProjectSettings(projectPath), dottedKey);
649
- }
650
902
  function getRepoSettings(gitCommonDir) {
651
903
  return loadConfig()?.repos?.[gitCommonDir]?.settings || {};
652
904
  }
653
- function readNested(obj, dottedKey) {
654
- if (!obj) return void 0;
655
- const keys = dottedKey.split(".");
656
- let cur = obj;
657
- for (const k of keys) {
658
- if (cur == null || typeof cur !== "object") return void 0;
659
- cur = cur[k];
660
- }
661
- return cur;
662
- }
663
905
  function isPathPrefix(prefix, path) {
664
906
  const head = comparablePath(prefix);
665
907
  const candidate = comparablePath(path);
@@ -685,11 +927,6 @@ function allMetroPorts() {
685
927
  if (!cfg?.projects) return [];
686
928
  return Object.values(cfg.projects).map((p) => p.metroPort).filter((p) => typeof p === "number");
687
929
  }
688
- function findProjectByMetroPort(port) {
689
- const cfg = loadConfig();
690
- for (const [path, proj] of Object.entries(cfg?.projects || {})) if (proj.metroPort === port) return path;
691
- return null;
692
- }
693
930
  //#endregion
694
931
  //#region state/tailnet.ts
695
932
  const DEFAULT_SERVE_PORT = 7443;
@@ -716,16 +953,25 @@ function findPeer(status, name) {
716
953
  return [{
717
954
  nodeId: peer.ID,
718
955
  dnsName,
719
- address
956
+ address,
957
+ ...typeof peer.OS === "string" && peer.OS ? { os: peer.OS } : {}
720
958
  }];
721
959
  });
722
960
  if (found.length === 0) return "missing";
723
961
  return found.length === 1 ? found[0] : "ambiguous";
724
962
  }
725
963
  /**
726
- * Connects to the pinned node's own tailnet address rather than resolving its name, so the socket reaches the
727
- * node whose ID was just checked. `tailscale serve` still needs the MagicDNS name for TLS (SNI and certificate)
728
- * and for its Host routing.
964
+ * Whether `tailscale status` reports the peer a `remote.machines` entry names as running an OS other than macOS.
965
+ * Such a machine can build but never host simulators or emulators; an unknown OS counts as able to host.
966
+ */
967
+ function cannotHost(status, entry) {
968
+ const parsed = parseMachine(entry);
969
+ const peer = parsed ? findPeer(status, parsed.name) : "missing";
970
+ return typeof peer === "object" && peer.os !== void 0 && peer.os !== "macOS";
971
+ }
972
+ /**
973
+ * Connects to the node's own tailnet address from `tailscale status`. `tailscale serve` still needs the MagicDNS name
974
+ * for TLS (SNI and certificate) and for its Host routing.
729
975
  */
730
976
  function endpoint(peer, port) {
731
977
  const address = peer.address.includes(":") ? `[${peer.address}]` : peer.address;
@@ -737,24 +983,30 @@ function endpoint(peer, port) {
737
983
  };
738
984
  }
739
985
  /**
740
- * The endpoint of a paired machine's pinned node, or why Stim does not connect to it. The name must still
741
- * resolve to exactly the pinned node; the token is never sent anywhere else.
986
+ * Where to reach a `remote.machines` entry now, or why Stim cannot. The tailnet only carries the bytes: the Noise
987
+ * handshake, pinned to the worker's server key, decides whether the machine answering is the one this machine paired
988
+ * with.
742
989
  */
743
- function pinnedEndpoint(credential, current) {
744
- const parsed = parseMachine(credential.machine);
745
- if (!parsed) return `${credential.machine} is not a tailnet name`;
990
+ function machineEndpoint(machine, current) {
991
+ const parsed = parseMachine(machine);
992
+ if (!parsed) return `${machine} is not a tailnet name`;
746
993
  if (!isJsonObject(current)) return "Tailscale is not running";
747
994
  const peer = findPeer(current, parsed.name);
748
995
  if (peer === "missing") return `no peer on this tailnet is named ${parsed.name}`;
749
996
  if (peer === "ambiguous") return `several tailnet peers match ${parsed.name}`;
750
- if (peer.nodeId !== credential.nodeId) return `${peer.dnsName} is now node ${peer.nodeId}, not the pinned ${credential.nodeId}; run stim doctor`;
751
997
  return endpoint(peer, parsed.port);
752
998
  }
753
999
  //#endregion
754
1000
  //#region state/hosted-macos.ts
755
1001
  /** App slots one host offers; a slot names the host-assigned bundle id `<bundleId>.hosted<slot>`. */
756
1002
  const HOSTED_MACOS_APP_SLOTS = 64;
1003
+ /** The drivers `hosting.agentDriver` can name: `none` and the first-party driver packages stim-server ships. */
757
1004
  const HOSTED_AGENT_DRIVERS = ["none", "agent-device"];
1005
+ const AGENT_DRIVER_NAME = /^[a-z][a-z0-9-]{0,62}$/;
1006
+ /** A driver name a grant or access record can carry; a client resolves only the drivers it knows. */
1007
+ function hostedAgentDriverName(value) {
1008
+ return typeof value === "string" && value !== "none" && AGENT_DRIVER_NAME.test(value);
1009
+ }
758
1010
  function hostedMacosAppSlot(value) {
759
1011
  return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 64;
760
1012
  }
@@ -815,14 +1067,17 @@ function parseHostedAgentLease(value) {
815
1067
  function parseHostedAgentGrant(value) {
816
1068
  if (!isJsonObject(value)) return null;
817
1069
  if (value.driver === "none") return Object.keys(value).length === 1 ? { driver: "none" } : null;
818
- if (!HOSTED_AGENT_DRIVERS.includes(value.driver) || typeof value.path !== "string" || !AGENT_PATH.test(value.path) || typeof value.token !== "string" || !/^[A-Za-z0-9_-]{32,256}$/.test(value.token) || typeof value.scope !== "string" || !/^[A-Za-z0-9._:-]{1,256}$/.test(value.scope) || Object.keys(value).length !== 5) return null;
819
- const lease = parseHostedAgentLease(value.lease);
820
- if (!lease) return null;
821
- return {
1070
+ if (!hostedAgentDriverName(value.driver) || typeof value.path !== "string" || !AGENT_PATH.test(value.path) || typeof value.token !== "string" || !/^[A-Za-z0-9_-]{32,256}$/.test(value.token) || typeof value.scope !== "string" || !/^[A-Za-z0-9._:-]{1,256}$/.test(value.scope) || Object.keys(value).length !== (value.lease === void 0 ? 4 : 5)) return null;
1071
+ const grant = {
822
1072
  driver: value.driver,
823
1073
  path: value.path,
824
1074
  token: value.token,
825
- scope: value.scope,
1075
+ scope: value.scope
1076
+ };
1077
+ if (value.lease === void 0) return grant;
1078
+ const lease = parseHostedAgentLease(value.lease);
1079
+ return lease && {
1080
+ ...grant,
826
1081
  lease: { ...lease }
827
1082
  };
828
1083
  }
@@ -832,9 +1087,9 @@ function parseHostedAgentAccess(value) {
832
1087
  driver: "none",
833
1088
  setting: "hosting.agentDriver"
834
1089
  } : null;
835
- if (value.driver !== "agent-device" || typeof value.remoteConfig !== "string" || !isAbsolute(value.remoteConfig) || typeof value.command !== "string" || !value.command || Object.keys(value).length !== 3) return null;
1090
+ if (!hostedAgentDriverName(value.driver) || typeof value.remoteConfig !== "string" || !isAbsolute(value.remoteConfig) || typeof value.command !== "string" || !value.command || Object.keys(value).length !== 3) return null;
836
1091
  return {
837
- driver: "agent-device",
1092
+ driver: value.driver,
838
1093
  remoteConfig: value.remoteConfig,
839
1094
  command: value.command
840
1095
  };
@@ -1011,7 +1266,7 @@ const SETTINGS = [
1011
1266
  patternHelp: "eas, proxy, auto, local, or a tailnet machine name"
1012
1267
  },
1013
1268
  scopes: EVERY,
1014
- 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; local runs here, overriding a lower layer"
1269
+ description: "Default iOS remote target: eas, proxy, or an approved Mac in remote.machines; auto places on an approved Mac when this machine is full or busy; local runs here, overriding a lower layer"
1015
1270
  },
1016
1271
  {
1017
1272
  key: "ios.projectPath",
@@ -1042,6 +1297,36 @@ const SETTINGS = [
1042
1297
  scopes: PROJECT,
1043
1298
  description: "SHA-1 of the signing identity, when two share a name"
1044
1299
  },
1300
+ {
1301
+ key: "ios.appStoreConnectKeyPath",
1302
+ type: ABSOLUTE_PATH,
1303
+ scopes: MACHINE,
1304
+ env: "STIM_ASC_KEY_PATH",
1305
+ sensitive: true,
1306
+ description: "App Store Connect API key (.p8) that `stim device provision ios` authenticates with"
1307
+ },
1308
+ {
1309
+ key: "ios.appStoreConnectKeyId",
1310
+ type: {
1311
+ kind: "string",
1312
+ pattern: "^[A-Za-z0-9]+$",
1313
+ patternHelp: "the key ID App Store Connect shows"
1314
+ },
1315
+ scopes: MACHINE,
1316
+ env: "STIM_ASC_KEY_ID",
1317
+ description: "Key ID of ios.appStoreConnectKeyPath"
1318
+ },
1319
+ {
1320
+ key: "ios.appStoreConnectIssuerId",
1321
+ type: {
1322
+ kind: "string",
1323
+ pattern: "^[A-Za-z0-9-]+$",
1324
+ patternHelp: "the issuer ID App Store Connect shows"
1325
+ },
1326
+ scopes: MACHINE,
1327
+ env: "STIM_ASC_ISSUER_ID",
1328
+ description: "Issuer ID of ios.appStoreConnectKeyPath"
1329
+ },
1045
1330
  {
1046
1331
  key: "ios.lanHost",
1047
1332
  type: {
@@ -1117,6 +1402,12 @@ const SETTINGS = [
1117
1402
  scopes: PROJECT,
1118
1403
  description: "Gradle build variant"
1119
1404
  },
1405
+ {
1406
+ key: "android.artifactInputs",
1407
+ type: OBJECT,
1408
+ scopes: PROJECT,
1409
+ description: "Complete repeatable native Gradle inputs for local APK caching, including declared local files and environment names"
1410
+ },
1120
1411
  {
1121
1412
  key: "android.offloadInputs",
1122
1413
  type: OBJECT,
@@ -1144,7 +1435,7 @@ const SETTINGS = [
1144
1435
  patternHelp: "eas, proxy, auto, local, or a tailnet machine name"
1145
1436
  },
1146
1437
  scopes: EVERY,
1147
- 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; local runs here, overriding a lower layer"
1438
+ description: "Default Android remote target: eas, proxy, or an approved Mac in remote.machines; auto places on an approved Mac when this machine is full or busy; local runs here, overriding a lower layer"
1148
1439
  },
1149
1440
  {
1150
1441
  key: "metro.tunnel",
@@ -1647,6 +1938,18 @@ const SETTINGS = [
1647
1938
  env: "STIM_MAINTENANCE_OLDER_THAN_DAYS",
1648
1939
  description: "Age in days for the age sweep: unused workspace build outputs and shared cache entries"
1649
1940
  },
1941
+ {
1942
+ key: "maintenance.deviceIdleDays",
1943
+ type: {
1944
+ kind: "number",
1945
+ integer: true,
1946
+ minimum: 0
1947
+ },
1948
+ scopes: MACHINE,
1949
+ default: 3,
1950
+ env: "STIM_MAINTENANCE_DEVICE_IDLE_DAYS",
1951
+ description: "Days a workspace stays idle before the age sweep deletes its owned simulators and emulators; 0 turns that off"
1952
+ },
1650
1953
  {
1651
1954
  key: "maintenance.protectRecentHours",
1652
1955
  type: {
@@ -1659,20 +1962,60 @@ const SETTINGS = [
1659
1962
  env: "STIM_MAINTENANCE_PROTECT_RECENT_HOURS",
1660
1963
  description: "Cache entries used within this many hours are never evicted"
1661
1964
  },
1965
+ {
1966
+ key: "maintenance.urgentFreeDiskGb",
1967
+ type: GIGABYTES,
1968
+ scopes: MACHINE,
1969
+ default: 12,
1970
+ env: "STIM_MAINTENANCE_URGENT_FREE_DISK_GB",
1971
+ description: "Free disk, in GB, below which automatic maintenance reports disk pressure level 2; 0 turns that threshold off"
1972
+ },
1973
+ {
1974
+ key: "maintenance.criticalFreeDiskGb",
1975
+ type: GIGABYTES,
1976
+ scopes: MACHINE,
1977
+ default: 6,
1978
+ env: "STIM_MAINTENANCE_CRITICAL_FREE_DISK_GB",
1979
+ description: "Free disk, in GB, below which automatic maintenance enters disk pressure level 3, also at or below budget.hardFloorDiskGb; 0 leaves only the hard floor"
1980
+ },
1981
+ {
1982
+ key: "maintenance.pressureProtectMinutes",
1983
+ type: {
1984
+ kind: "number",
1985
+ integer: true,
1986
+ minimum: 0
1987
+ },
1988
+ scopes: MACHINE,
1989
+ default: 10,
1990
+ env: "STIM_MAINTENANCE_PRESSURE_PROTECT_MINUTES",
1991
+ description: "Protection window, in minutes, that replaces maintenance.protectRecentHours at disk pressure levels 2 and 3"
1992
+ },
1993
+ {
1994
+ key: "maintenance.gradleVersionIdleDays",
1995
+ type: {
1996
+ kind: "number",
1997
+ integer: true,
1998
+ minimum: 0
1999
+ },
2000
+ scopes: MACHINE,
2001
+ default: 7,
2002
+ env: "STIM_MAINTENANCE_GRADLE_VERSION_IDLE_DAYS",
2003
+ description: "At disk pressure level 3, stop idle Gradle and Kotlin daemons, then delete the Gradle version caches and wrapper distributions no registered project pins once their directories are unmodified this many days; 0 turns this off"
2004
+ },
1662
2005
  {
1663
2006
  key: "maintenance.removeFinishedWorktrees",
1664
2007
  type: BOOLEAN,
1665
2008
  scopes: MACHINE,
1666
2009
  default: true,
1667
2010
  env: "STIM_MAINTENANCE_REMOVE_FINISHED_WORKTREES",
1668
- description: "Remove clean linked worktrees whose branch or pull request finished, under gc.worktreeGraceMinutes"
2011
+ description: "Remove clean linked worktrees whose branch or pull request finished, under gc.worktreeGraceMinutes, and at disk pressure level 3 clean, pushed ones with no commits of their own"
1669
2012
  },
1670
2013
  {
1671
2014
  key: "maintenance.keep",
1672
2015
  type: BOOLEAN,
1673
2016
  scopes: PROJECT,
1674
2017
  default: false,
1675
- description: "Pin this workspace: automatic maintenance never clears its build outputs or removes its worktree"
2018
+ description: "Pin this workspace: automatic maintenance never clears its build outputs, deletes its owned devices or removes its worktree"
1676
2019
  },
1677
2020
  {
1678
2021
  key: "maintenance.workspaceOutputsMaxGb",
@@ -1761,7 +2104,7 @@ const SETTINGS = [
1761
2104
  scopes: MACHINE,
1762
2105
  default: false,
1763
2106
  env: "STIM_DEBUG",
1764
- description: "Debug-level logging. While on, the CLI records its commands, child processes and requests to other Macs, and stim-server records every request with its timings, under STIM_HOME/logs/debug/ with rotation. Nothing is sent anywhere, and records never carry arguments, tokens or tickets"
2107
+ description: "Debug-level logging. While on, the CLI records its commands, child processes and requests to other machines, and stim-server records every request with its timings, under STIM_HOME/logs/debug/ with rotation. Nothing is sent anywhere, and records never carry arguments, tokens or tickets"
1765
2108
  },
1766
2109
  {
1767
2110
  key: "hosting.agentDriver",
@@ -1773,18 +2116,32 @@ const SETTINGS = [
1773
2116
  default: "none",
1774
2117
  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"
1775
2118
  },
2119
+ {
2120
+ key: "server.buildsPaused",
2121
+ type: { kind: "boolean" },
2122
+ scopes: MACHINE,
2123
+ default: false,
2124
+ description: "Temporarily refuse new incoming builds without interrupting running builds, serving cached artifacts, or changing client approvals; local CLI builds remain available"
2125
+ },
2126
+ {
2127
+ key: "server.devicesPaused",
2128
+ type: { kind: "boolean" },
2129
+ scopes: MACHINE,
2130
+ default: false,
2131
+ description: "Temporarily refuse new hosted iOS, Android and macOS sessions without interrupting existing sessions or changing client approvals; local CLI devices remain available"
2132
+ },
1776
2133
  {
1777
2134
  key: "server.acceptClientBuilds",
1778
2135
  type: { kind: "boolean" },
1779
2136
  scopes: MACHINE,
1780
2137
  default: false,
1781
- 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"
2138
+ description: "Whether a machine approved for builds or device hosting here may update this machine's stim-server service to that machine's own stim-server build; releases from npm need no setting"
1782
2139
  },
1783
2140
  {
1784
2141
  key: "remote.machines",
1785
2142
  type: { kind: "strings" },
1786
2143
  scopes: MACHINE,
1787
- 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"
2144
+ description: "Tailscale names of the remote machines this machine 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"
1788
2145
  },
1789
2146
  {
1790
2147
  key: "remote.buildPoolDisabled",
@@ -1805,7 +2162,7 @@ const SETTINGS = [
1805
2162
  type: BOOLEAN,
1806
2163
  scopes: EVERY,
1807
2164
  default: false,
1808
- description: "When ios.remote or android.remote is auto, this Mac is at its concurrency.maxDevices cap and no Mac in remote.machines takes the run, run the simulator or emulator on a billed EAS Simulator instead of waiting or refusing, when eas-cli can"
2165
+ description: "When ios.remote or android.remote is auto, this machine is at its concurrency.maxDevices cap and no remote.machines entry takes the run, run the simulator or emulator on a billed EAS Simulator instead of waiting or refusing, when eas-cli can"
1809
2166
  },
1810
2167
  {
1811
2168
  key: "remote.build",
@@ -1817,7 +2174,7 @@ const SETTINGS = [
1817
2174
  scopes: MACHINE,
1818
2175
  default: "auto",
1819
2176
  env: "STIM_REMOTE_BUILD",
1820
- description: "Build placement: auto follows remote.buildMode within enabled automatic pool members; 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"
2177
+ description: "Build placement: auto follows remote.buildMode within enabled automatic pool members; local always builds here; a name in remote.machines requires that remote machine without fallback. The ios, android and macos --remote-build flag overrides this setting and its environment override"
1821
2178
  },
1822
2179
  {
1823
2180
  key: "remote.buildMode",
@@ -1828,7 +2185,7 @@ const SETTINGS = [
1828
2185
  scopes: MACHINE,
1829
2186
  default: "auto",
1830
2187
  env: "STIM_REMOTE_BUILD_MODE",
1831
- 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 an enabled remote Mac whenever one can take the build; off builds here when local is enabled in remote.buildPoolDisabled"
2188
+ description: "When remote.build is auto, where iOS simulator Debug, Android emulator debug and macOS SwiftPM Debug builds run: auto builds here while this machine has a free concurrency.maxBuilds slot and its load is under server.maxLoadPerCore, and otherwise builds on a less loaded machine in remote.machines; force builds on an enabled remote machine whenever one can take the build; off builds here when local is enabled in remote.buildPoolDisabled"
1832
2189
  },
1833
2190
  {
1834
2191
  key: "server.maxLoadPerCore",
@@ -1838,13 +2195,13 @@ const SETTINGS = [
1838
2195
  },
1839
2196
  scopes: MACHINE,
1840
2197
  default: 2,
1841
- 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"
2198
+ description: "Load per core (5-minute load average divided by the CPU count) at which this machine counts as saturated: it declines builds other machines send it, automatic build placement stops preferring it, and automatic iOS and Android device placement treats it as busy"
1842
2199
  },
1843
2200
  {
1844
2201
  key: "server.workerRoot",
1845
2202
  type: ABSOLUTE_PATH,
1846
2203
  scopes: MACHINE,
1847
- description: "Directory where stim-server keeps the checkouts, dependencies and build state of the Macs that build here; default $STIM_HOME/build-worker"
2204
+ description: "Directory where stim-server keeps the checkouts, dependencies and build state of the machines that build here; default $STIM_HOME/build-worker"
1848
2205
  },
1849
2206
  {
1850
2207
  key: "server.gradleDaemonIdleMinutes",
@@ -1856,7 +2213,7 @@ const SETTINGS = [
1856
2213
  },
1857
2214
  scopes: MACHINE,
1858
2215
  default: 30,
1859
- 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"
2216
+ description: "Minutes the Gradle daemon of an Android build another machine sent here stays warm after the build; 0 stops it when the build ends"
1860
2217
  },
1861
2218
  {
1862
2219
  key: "caches.buildCache",
@@ -1888,9 +2245,6 @@ function isLayeredSetting(setting) {
1888
2245
  return setting.scopes.some((scope) => scope !== "machine");
1889
2246
  }
1890
2247
  const SETTING_GROUPS = [...new Set(SETTINGS.filter(isLayeredSetting).flatMap((setting) => setting.key.split(".").slice(0, -1).map((_, index, parts) => parts.slice(0, index + 1).join("."))))];
1891
- function isPlainObject(value) {
1892
- return value !== null && typeof value === "object" && !Array.isArray(value);
1893
- }
1894
2248
  function bundleUrlAccepts(value, key) {
1895
2249
  if (typeof value !== "string" || !/^(https?:\/\/|\/(?!\/))/.test(value) || /[\s\\#]/.test(value)) return false;
1896
2250
  try {
@@ -1921,7 +2275,7 @@ function acceptsShape(setting, value) {
1921
2275
  case "strings": return Array.isArray(value) && value.every((entry) => typeof entry === "string");
1922
2276
  case "number": return typeof value === "number";
1923
2277
  case "choice": return typeof value === "string" && type.choices.includes(value);
1924
- case "object": return isPlainObject(value);
2278
+ case "object": return isJsonObject(value);
1925
2279
  case "bundle-url": return bundleUrlAccepts(value, setting.key);
1926
2280
  }
1927
2281
  }
@@ -2026,7 +2380,7 @@ function tryAcquireBuildSlotClaim({ max, details }) {
2026
2380
  if (refusal && !busy) throw refusal;
2027
2381
  return null;
2028
2382
  }
2029
- /** Build slots held for a remote Mac's offloaded builds, which run under another Stim home. */
2383
+ /** Build slots held for a remote machine's offloaded builds, which run under another Stim home. */
2030
2384
  function liveOffloadedSlots() {
2031
2385
  let names;
2032
2386
  try {
@@ -2087,7 +2441,7 @@ function machineCapacity() {
2087
2441
  maxLoadPerCore: maxLoadPerCore()
2088
2442
  };
2089
2443
  }
2090
- /** Why a Mac with this capacity should not take another native build; null when it can. */
2444
+ /** Why a machine with this capacity should not take another native build; null when it can. */
2091
2445
  function saturation(capacity) {
2092
2446
  if (capacity.maxBuilds > 0 && capacity.builds >= capacity.maxBuilds) return `all ${capacity.maxBuilds} build slots busy`;
2093
2447
  if (capacity.loadPerCore >= capacity.maxLoadPerCore) return `load at or above ${capacity.maxLoadPerCore}/core`;
@@ -2095,41 +2449,55 @@ function saturation(capacity) {
2095
2449
  }
2096
2450
  //#endregion
2097
2451
  //#region state/build-machines.ts
2098
- function parseCredential(value) {
2452
+ const MACHINE_CREDENTIALS_VERSION = 2;
2453
+ const KEY = /^[A-Za-z0-9_-]{43}$/;
2454
+ /** A stored credential, or null when it is malformed or from an older version. */
2455
+ function parseMachineCredential(value) {
2099
2456
  if (!isJsonObject(value)) return null;
2100
- const { machine, nodeId, dnsName, deviceId, deviceToken, state, requestedAt } = value;
2457
+ const { machine, transport, address, state, requestedAt, serverKey, deviceId, ticket, expiresAt } = value;
2101
2458
  if (![
2102
2459
  machine,
2103
- nodeId,
2104
- dnsName,
2105
- deviceId,
2106
- deviceToken,
2460
+ address,
2107
2461
  requestedAt
2108
2462
  ].every((field) => typeof field === "string" && field)) return null;
2109
- if (state !== "pending" && state !== "approved") return null;
2463
+ if (transport !== "tailscale" || state !== "pending" && state !== "approved") return null;
2464
+ if (serverKey !== void 0 && (typeof serverKey !== "string" || !KEY.test(serverKey))) return null;
2465
+ if (ticket !== void 0 && (typeof ticket !== "string" || !KEY.test(ticket))) return null;
2466
+ if (state === "approved" && (serverKey === void 0 || typeof deviceId !== "string" || !deviceId)) return null;
2467
+ if (state === "pending" && ticket === void 0) return null;
2110
2468
  return {
2111
2469
  machine,
2112
- nodeId,
2113
- dnsName,
2114
- deviceId,
2115
- deviceToken,
2470
+ transport,
2471
+ address,
2116
2472
  state,
2117
2473
  requestedAt,
2118
- ...typeof value.expiresAt === "string" && value.expiresAt ? { expiresAt: value.expiresAt } : {},
2119
- ...typeof value.ticketHash === "string" ? { ticketHash: value.ticketHash } : {}
2474
+ ...serverKey ? { serverKey } : {},
2475
+ ...typeof deviceId === "string" && deviceId ? { deviceId } : {},
2476
+ ...ticket ? { ticket } : {},
2477
+ ...typeof expiresAt === "string" && expiresAt ? { expiresAt } : {}
2120
2478
  };
2121
2479
  }
2122
2480
  function readBuildMachines() {
2123
2481
  try {
2124
- const machines = readJsonFile(buildMachinesFile()).machines;
2125
- return Array.isArray(machines) ? machines.flatMap((entry) => parseCredential(entry) ?? []) : [];
2482
+ const store = readJsonFile(buildMachinesFile());
2483
+ if (store.version !== 2 || !Array.isArray(store.machines)) return [];
2484
+ return store.machines.flatMap((entry) => parseMachineCredential(entry) ?? []);
2126
2485
  } catch {
2127
2486
  return [];
2128
2487
  }
2129
2488
  }
2130
2489
  //#endregion
2490
+ //#region state/client-identity.ts
2491
+ /** This machine's long-term X25519 key as a client of other machines, in `$STIM_HOME/client-identity.key`. */
2492
+ function clientIdentity() {
2493
+ return readOrCreateKeyFile(clientIdentityKeyFile());
2494
+ }
2495
+ //#endregion
2131
2496
  //#region state/device-host-machines.ts
2132
- /** An absent store is empty; unreadable or malformed credentials refuse access without discarding pins. */
2497
+ /**
2498
+ * An absent store, or one from protocol 1, is empty; an unreadable or malformed one refuses access without discarding
2499
+ * pinned keys.
2500
+ */
2133
2501
  function readDeviceHostMachines() {
2134
2502
  let value;
2135
2503
  try {
@@ -2138,23 +2506,13 @@ function readDeviceHostMachines() {
2138
2506
  if (error.code === "ENOENT") return [];
2139
2507
  throw error;
2140
2508
  }
2141
- if (!isJsonObject(value) || value.version !== 1 || !Array.isArray(value.machines)) throw new Error("Invalid device-host machine credential store");
2509
+ if (isJsonObject(value) && value.version === 1) return [];
2510
+ if (!isJsonObject(value) || value.version !== 2 || !Array.isArray(value.machines)) throw new Error("Invalid device-host machine credential store");
2142
2511
  const machines = [];
2143
2512
  for (const entry of value.machines) {
2144
- if (!isJsonObject(entry) || ![
2145
- "machine",
2146
- "nodeId",
2147
- "dnsName",
2148
- "deviceId",
2149
- "deviceToken",
2150
- "requestedAt"
2151
- ].every((key) => typeof entry[key] === "string" && entry[key]) || entry.state !== "pending" && entry.state !== "approved" || machines.some((each) => each.machine === entry.machine)) throw new Error("Invalid device-host machine credential store");
2152
- const { ticketHash, expiresAt, ...credential } = entry;
2153
- machines.push({
2154
- ...credential,
2155
- ...typeof expiresAt === "string" && expiresAt ? { expiresAt } : {},
2156
- ...typeof ticketHash === "string" ? { ticketHash } : {}
2157
- });
2513
+ const credential = parseMachineCredential(entry);
2514
+ if (!credential || machines.some((each) => each.machine === credential.machine)) throw new Error("Invalid device-host machine credential store");
2515
+ machines.push(credential);
2158
2516
  }
2159
2517
  return machines;
2160
2518
  }
@@ -2174,11 +2532,15 @@ function requireAutomaticMachine(role, machine) {
2174
2532
  remedy: `Enable it in remote.${role}PoolDisabled or select a machine explicitly.`
2175
2533
  });
2176
2534
  }
2177
- function validateAutomaticPool(role, config) {
2535
+ /**
2536
+ * Refuses a pool with no enabled member: local, or a configured remote approved for the role. `canHost` excludes
2537
+ * device-pool remotes that can never host, such as a Linux machine.
2538
+ */
2539
+ function validateAutomaticPool(role, config, canHost = () => true) {
2178
2540
  if (automaticMachineEnabled(role, "local", config)) return;
2179
2541
  const configured = config.remote?.machines;
2180
2542
  const approved = role === "build" ? readBuildMachines() : readDeviceHostMachines();
2181
- if (Array.isArray(configured) && configured.some((machine) => typeof machine === "string" && automaticMachineEnabled(role, machine, config) && approved.some((entry) => entry.machine === machine && entry.state === "approved"))) return;
2543
+ if (Array.isArray(configured) && configured.some((machine) => typeof machine === "string" && automaticMachineEnabled(role, machine, config) && (role === "build" || canHost(machine)) && approved.some((entry) => entry.machine === machine && entry.state === "approved"))) return;
2182
2544
  throw Object.assign(/* @__PURE__ */ new Error(`Keep at least one enabled automatic ${role} pool member: local or a configured, approved remote machine.`), { code: "STIM_BAD_ARG" });
2183
2545
  }
2184
2546
  //#endregion
@@ -2192,7 +2554,7 @@ function debugLogDir() {
2192
2554
  return join(configDir(), "logs", "debug");
2193
2555
  }
2194
2556
  /** The `STIM_DEBUG` override (1/true on, 0/false off, anything else off), else the machine setting `debug.logs`. Never throws. */
2195
- function debugLoggingEnabled(env = process.env) {
2557
+ function debugLoggingEnabled(env = ambientEnv()) {
2196
2558
  const definition = settingDefinition("debug.logs");
2197
2559
  const raw = env[DEBUG_ENV];
2198
2560
  if (raw !== void 0 && raw !== "") {
@@ -2219,15 +2581,21 @@ function scrub(value, depth = 0) {
2219
2581
  * Debug-level records for one component (`cli` or `server`). Off by default; `STIM_DEBUG=1` or `debug.logs` turns it
2220
2582
  * on. Callers pass names, durations and codes, never arguments, environment values, tokens or tickets.
2221
2583
  */
2222
- function createDebugLog(component, { env = process.env, now = Date.now, base = () => ({}) } = {}) {
2584
+ function createDebugLog(component, { env, now = Date.now, base = () => ({}) } = {}) {
2223
2585
  let checkedAt = Number.NEGATIVE_INFINITY;
2224
2586
  let on = false;
2225
2587
  let unchecked = ROTATE_CHECK_BYTES;
2588
+ let checkedEnv;
2589
+ let checkedHome;
2226
2590
  const enabled = () => {
2227
2591
  const at = now();
2228
- if (at - checkedAt >= ENABLED_RECHECK_MS) {
2592
+ const activeEnv = env ?? ambientEnv();
2593
+ const home = configDir();
2594
+ if (activeEnv !== checkedEnv || home !== checkedHome || at - checkedAt >= ENABLED_RECHECK_MS) {
2229
2595
  checkedAt = at;
2230
- on = debugLoggingEnabled(env);
2596
+ checkedEnv = activeEnv;
2597
+ checkedHome = home;
2598
+ on = debugLoggingEnabled(activeEnv);
2231
2599
  }
2232
2600
  return on;
2233
2601
  };
@@ -2258,6 +2626,15 @@ function createDebugLog(component, { env = process.env, now = Date.now, base = (
2258
2626
  };
2259
2627
  }
2260
2628
  //#endregion
2629
+ //#region state/home-identity.ts
2630
+ /** The persistent, nonsecret identity of this Stim home; null until the CLI initializes it. */
2631
+ function readHomeIdentity(config = loadConfig()) {
2632
+ const id = config?.homeId;
2633
+ if (id === void 0) return null;
2634
+ if (!validHomeId(id)) throw Object.assign(/* @__PURE__ */ new Error(`Stim home identity is not a UUID. ${configCorruptRepair()}`), { code: "STIM_CONFIG_CORRUPT" });
2635
+ return id.toLowerCase();
2636
+ }
2637
+ //#endregion
2261
2638
  //#region state/ledgers.ts
2262
2639
  function ids(value) {
2263
2640
  return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry !== "") : [];
@@ -2692,7 +3069,8 @@ function expoContextFollower(onRecord, into) {
2692
3069
  return {
2693
3070
  push,
2694
3071
  settle,
2695
- flush
3072
+ flush,
3073
+ pending: () => pending !== null
2696
3074
  };
2697
3075
  }
2698
3076
  function includeBareErrorContext(all, matched, launchTs, sinceTs) {
@@ -2743,11 +3121,11 @@ function launchMarkersBySlot(records) {
2743
3121
  }
2744
3122
  return markers;
2745
3123
  }
2746
- function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, errorsOnly, errorContext, now } = {}) {
2747
- const source = records ?? readLogRecords(dir);
2748
- const slotLaunched = slot !== void 0 && source.some((record) => isLaunchMarker(record) && (record.slot ?? "default") === slot);
2749
- const all = source.filter((record) => slot === void 0 || (record.slot ?? "default") === slot || slotLaunched && isSharedRecord(record));
2750
- if (all.length === 0) return [];
3124
+ /** Decides which records a query matches from the timeline's marker records alone. */
3125
+ function planLogQuery(markers, { slot, sources, minLevel, since, grep, errorsOnly, now } = {}) {
3126
+ const slotLaunched = slot !== void 0 && markers.some((record) => isLaunchMarker(record) && (record.slot ?? "default") === slot);
3127
+ const inTimeline = (record) => slot === void 0 || (record.slot ?? "default") === slot || slotLaunched && isSharedRecord(record);
3128
+ const all = markers.filter((record) => record.marker === true && inTimeline(record));
2751
3129
  const { launchTs, bundleTs, pageLoadTs } = errorsOnly ? markerWindow(all) : {
2752
3130
  launchTs: null,
2753
3131
  bundleTs: null,
@@ -2765,9 +3143,8 @@ function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, e
2765
3143
  bundleMarkerTs: bundleTs === null ? void 0 : bundleTs
2766
3144
  });
2767
3145
  const slotMarkers = launchMarkersBySlot(all);
2768
- const markers = all.filter((record) => record.marker === true);
2769
- const launches = newestByPlatform(markers.filter((record) => record.src !== "metro" && !isWebPageRecord(record)), (record) => typeof record.platform === "string" ? record.platform : null);
2770
- const bundles = newestByPlatform(markers.filter((record) => record.src === "metro"), bundleMarkerPlatform);
3146
+ const launches = newestByPlatform(all.filter((record) => record.src !== "metro" && !isWebPageRecord(record)), (record) => typeof record.platform === "string" ? record.platform : null);
3147
+ const bundles = newestByPlatform(all.filter((record) => record.src === "metro"), bundleMarkerPlatform);
2771
3148
  const slotPlatforms = slot === void 0 ? [] : [...launches.keys()].filter((platform) => platform !== null);
2772
3149
  const windowStart = (record) => {
2773
3150
  if (pageLoadTs !== null && isWebPageRecord(record)) return pageLoadTs - 1;
@@ -2783,13 +3160,58 @@ function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, e
2783
3160
  const platform = nativePlatform(record);
2784
3161
  return newestWhere(bundles, (marker) => marker === null || (platform ? marker === platform : slotPlatforms.length === 0 || slotPlatforms.includes(marker)));
2785
3162
  };
2786
- let matched = all.filter((record) => !otherPlatform(record) && recordMatches(record, {
2787
- ...criteria,
2788
- markerTs: windowStart(record),
2789
- bundleMarkerTs: bundleWindowStart(record)
2790
- }));
3163
+ const lowestWindowStart = (slots) => {
3164
+ if (!criteria.errorsOnly) return void 0;
3165
+ const starts = [];
3166
+ if (pageLoadTs !== null) starts.push(pageLoadTs - 1);
3167
+ const kinds = criteria.sources ?? [];
3168
+ if (criteria.includeAppDeviceErrors || kinds.some((src) => src !== "metro" && src !== "client")) {
3169
+ const possible = slot !== void 0 ? [slot] : slots ? [...slots] : null;
3170
+ if (!possible) return void 0;
3171
+ for (const value of possible) {
3172
+ const start = slotMarkers.get(value);
3173
+ if (start === void 0) return void 0;
3174
+ starts.push(start);
3175
+ }
3176
+ }
3177
+ if (kinds.includes("metro") || kinds.includes("client")) {
3178
+ if (criteria.markerTs === void 0) return void 0;
3179
+ starts.push(criteria.markerTs, ...launches.values());
3180
+ }
3181
+ return starts.length ? Math.min(...starts) : void 0;
3182
+ };
3183
+ return {
3184
+ inTimeline,
3185
+ matches: (record) => inTimeline(record) && !otherPlatform(record) && recordMatches(record, {
3186
+ ...criteria,
3187
+ markerTs: windowStart(record),
3188
+ bundleMarkerTs: bundleWindowStart(record)
3189
+ }),
3190
+ launchTs,
3191
+ sinceTs: criteria.sinceTs,
3192
+ excludesUpTo: (maxTs, slots) => {
3193
+ if (criteria.sinceTs !== void 0 && maxTs < criteria.sinceTs) return true;
3194
+ const start = lowestWindowStart(slots);
3195
+ return start !== void 0 && maxTs <= start;
3196
+ }
3197
+ };
3198
+ }
3199
+ function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, errorsOnly, errorContext, now } = {}) {
3200
+ const source = records ?? readLogRecords(dir);
3201
+ const plan = planLogQuery(source.filter((record) => record.marker === true), {
3202
+ slot,
3203
+ sources,
3204
+ minLevel,
3205
+ since,
3206
+ grep,
3207
+ errorsOnly,
3208
+ now
3209
+ });
3210
+ const all = source.filter(plan.inTimeline);
3211
+ if (all.length === 0) return [];
3212
+ let matched = all.filter(plan.matches);
2791
3213
  if (typeof tail === "number" && tail >= 0 && matched.length > tail) matched = matched.slice(matched.length - tail);
2792
- return errorContext ? includeBareErrorContext(all, attachExpoErrorContext(all, matched), launchTs, criteria.sinceTs) : matched;
3214
+ return errorContext ? includeBareErrorContext(all, attachExpoErrorContext(all, matched), plan.launchTs, plan.sinceTs) : matched;
2793
3215
  }
2794
3216
  /** Queries the JSON timeline, including Expo context fields on errors as `stim logs --json` does. */
2795
3217
  function queryJsonLogs(options = {}) {
@@ -2834,10 +3256,12 @@ function advanceTail(prev, chunk, size) {
2834
3256
  records
2835
3257
  };
2836
3258
  }
2837
- function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets = null, errorContext }) {
3259
+ function followLogs({ dir, onRecord, criteria = {}, intervalMs, offsets = null, errorContext }) {
2838
3260
  const follower = errorContext ? expoContextFollower(onRecord, errorContext) : null;
2839
3261
  const state = /* @__PURE__ */ new Map();
2840
3262
  const decoders = /* @__PURE__ */ new Map();
3263
+ let stopped = false;
3264
+ let contextTimer;
2841
3265
  for (const [name, size] of Object.entries(offsets || fileSizes(dir))) {
2842
3266
  state.set(name, {
2843
3267
  offset: size,
@@ -2847,6 +3271,7 @@ function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets =
2847
3271
  }
2848
3272
  function emit(records) {
2849
3273
  for (const record of records) {
3274
+ if (stopped) break;
2850
3275
  const matches = recordMatches(record, criteria);
2851
3276
  if (follower) follower.push(record, matches);
2852
3277
  else if (matches) onRecord(record);
@@ -2876,14 +3301,105 @@ function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets =
2876
3301
  emit(next.records);
2877
3302
  }
2878
3303
  function poll() {
2879
- for (const name of logFiles(dir)) try {
2880
- pollFile(name);
2881
- } catch {}
3304
+ if (stopped) return;
3305
+ for (const name of logFiles(dir)) {
3306
+ if (stopped) break;
3307
+ try {
3308
+ pollFile(name);
3309
+ } catch {}
3310
+ }
3311
+ if (intervalMs !== void 0) follower?.settle();
3312
+ else if (!contextTimer && follower?.pending()) {
3313
+ follower.settle();
3314
+ contextTimer = setTimeout(settleContext, 500);
3315
+ }
3316
+ }
3317
+ function settleContext() {
3318
+ if (stopped) return;
3319
+ poll();
3320
+ contextTimer = void 0;
3321
+ if (stopped) return;
2882
3322
  follower?.settle();
3323
+ if (follower?.pending()) contextTimer = setTimeout(settleContext, 500);
3324
+ }
3325
+ let watcher;
3326
+ let directory;
3327
+ let scheduled;
3328
+ let repairTimer;
3329
+ function closeWatch() {
3330
+ const previous = watcher;
3331
+ watcher = void 0;
3332
+ directory = void 0;
3333
+ previous?.close();
3334
+ }
3335
+ function schedulePoll() {
3336
+ if (stopped || scheduled) return;
3337
+ scheduled = setImmediate(() => {
3338
+ scheduled = void 0;
3339
+ poll();
3340
+ });
3341
+ }
3342
+ function sameDirectory() {
3343
+ if (!watcher || !directory) return false;
3344
+ try {
3345
+ const current = statSync(dir);
3346
+ return current.dev === directory.dev && current.ino === directory.ino;
3347
+ } catch {
3348
+ return false;
3349
+ }
3350
+ }
3351
+ function attachWatch() {
3352
+ if (stopped || watcher) return false;
3353
+ try {
3354
+ const current = statSync(dir);
3355
+ const next = watch(dir, { persistent: false }, (event) => {
3356
+ if (stopped) return;
3357
+ if (event === "rename" && !sameDirectory()) {
3358
+ closeWatch();
3359
+ armRepair(attachWatch() ? 500 : void 0);
3360
+ }
3361
+ schedulePoll();
3362
+ });
3363
+ watcher = next;
3364
+ directory = current;
3365
+ const lost = () => {
3366
+ if (watcher !== next) return;
3367
+ closeWatch();
3368
+ armRepair();
3369
+ };
3370
+ next.once("error", lost);
3371
+ next.once("close", lost);
3372
+ return true;
3373
+ } catch {}
3374
+ return false;
3375
+ }
3376
+ function armRepair(delay = watcher ? 1e4 : 500) {
3377
+ clearTimeout(repairTimer);
3378
+ if (stopped) return;
3379
+ repairTimer = setTimeout(() => {
3380
+ let attached = false;
3381
+ if (!sameDirectory()) {
3382
+ closeWatch();
3383
+ attached = attachWatch();
3384
+ }
3385
+ poll();
3386
+ armRepair(attached ? 500 : void 0);
3387
+ }, delay);
3388
+ }
3389
+ const timer = intervalMs === void 0 ? void 0 : setInterval(poll, intervalMs);
3390
+ if (intervalMs === void 0) {
3391
+ attachWatch();
3392
+ poll();
3393
+ armRepair(500);
2883
3394
  }
2884
- const timer = setInterval(poll, intervalMs);
2885
3395
  return function stop() {
3396
+ if (stopped) return;
3397
+ stopped = true;
2886
3398
  clearInterval(timer);
3399
+ clearTimeout(repairTimer);
3400
+ clearTimeout(contextTimer);
3401
+ clearImmediate(scheduled);
3402
+ closeWatch();
2887
3403
  follower?.flush();
2888
3404
  };
2889
3405
  }
@@ -2912,6 +3428,378 @@ function sortByTs(records) {
2912
3428
  });
2913
3429
  }
2914
3430
  //#endregion
3431
+ //#region state/log-reader.ts
3432
+ const BLOCK_BYTES = 65536;
3433
+ const MAX_CACHED_GENERATIONS = 512;
3434
+ const NEWLINE = 10;
3435
+ const VALIDATE_BYTES = 256;
3436
+ const cache = /* @__PURE__ */ new Map();
3437
+ const indexing = /* @__PURE__ */ new Map();
3438
+ function tsKey(record) {
3439
+ return typeof record.ts === "number" && Number.isFinite(record.ts) ? record.ts : Infinity;
3440
+ }
3441
+ function compare(a, b) {
3442
+ return a.ts - b.ts || a.rank - b.rank || a.generation - b.generation || a.position - b.position;
3443
+ }
3444
+ function markerTs(record) {
3445
+ const ts = tsKey(record);
3446
+ return ts === Infinity ? -Infinity : ts;
3447
+ }
3448
+ function keepNewer(markers, key, marker) {
3449
+ const known = markers.get(key);
3450
+ if (!known || markerTs(marker) > markerTs(known)) markers.set(key, marker);
3451
+ }
3452
+ function markerKey(record) {
3453
+ const platform = record.src === "metro" ? bundleMarkerPlatform(record) : typeof record.platform === "string" ? record.platform : null;
3454
+ const ts = tsKey(record);
3455
+ return {
3456
+ key: JSON.stringify([
3457
+ record.src ?? null,
3458
+ record.slot === void 0 ? 0 : [record.slot],
3459
+ platform
3460
+ ]),
3461
+ marker: {
3462
+ src: record.src,
3463
+ marker: true,
3464
+ ...ts === Infinity ? {} : { ts },
3465
+ ...record.slot === void 0 ? {} : { slot: record.slot },
3466
+ ...platform === null ? {} : { platform }
3467
+ }
3468
+ };
3469
+ }
3470
+ function emptyIndex() {
3471
+ return {
3472
+ size: 0,
3473
+ head: Buffer.alloc(0),
3474
+ tail: Buffer.alloc(0),
3475
+ blocks: [],
3476
+ minTs: null,
3477
+ markers: /* @__PURE__ */ new Map(),
3478
+ launchAttempts: [],
3479
+ slots: /* @__PURE__ */ new Set()
3480
+ };
3481
+ }
3482
+ function indexBlock(index, buffer, start) {
3483
+ let maxTs = -Infinity;
3484
+ let minTs = Infinity;
3485
+ const sources = /* @__PURE__ */ new Set();
3486
+ const levels = /* @__PURE__ */ new Set();
3487
+ for (let lineStart = 0; lineStart < buffer.length;) {
3488
+ const lineEnd = buffer.indexOf(NEWLINE, lineStart);
3489
+ const record = parseNdjsonLine(buffer.toString("utf8", lineStart, lineEnd));
3490
+ if (record) {
3491
+ const value = tsKey(record);
3492
+ maxTs = Math.max(maxTs, value);
3493
+ minTs = Math.min(minTs, value);
3494
+ if (value !== Infinity && (index.minTs === null || value < index.minTs)) index.minTs = value;
3495
+ sources.add(record.src);
3496
+ levels.add(record.level);
3497
+ index.slots.add(record.slot ?? "default");
3498
+ if (record.marker === true) {
3499
+ const { key, marker } = markerKey(record);
3500
+ keepNewer(index.markers, key, marker);
3501
+ }
3502
+ if (record.event === "launch_attempt") index.launchAttempts.push({
3503
+ record,
3504
+ position: start + lineStart
3505
+ });
3506
+ }
3507
+ lineStart = lineEnd + 1;
3508
+ }
3509
+ return {
3510
+ start,
3511
+ end: start + buffer.length,
3512
+ maxTs,
3513
+ minTs,
3514
+ sources,
3515
+ levels
3516
+ };
3517
+ }
3518
+ async function read(handle, position, length) {
3519
+ const buffer = Buffer.allocUnsafe(length);
3520
+ let filled = 0;
3521
+ while (filled < length) {
3522
+ const { bytesRead } = await handle.read(buffer, filled, length - filled, position + filled);
3523
+ if (bytesRead === 0) break;
3524
+ filled += bytesRead;
3525
+ }
3526
+ return buffer.subarray(0, filled);
3527
+ }
3528
+ async function stillExtends(handle, index, size) {
3529
+ if (size < index.size) return false;
3530
+ const head = await read(handle, 0, index.head.length);
3531
+ const tail = await read(handle, index.size - index.tail.length, index.tail.length);
3532
+ return head.equals(index.head) && tail.equals(index.tail);
3533
+ }
3534
+ function resumeFrom(index) {
3535
+ const last = index.blocks.at(-1);
3536
+ if (!last || last.end - last.start >= BLOCK_BYTES) return {
3537
+ ...index,
3538
+ blocks: [...index.blocks]
3539
+ };
3540
+ return {
3541
+ ...index,
3542
+ size: last.start,
3543
+ blocks: index.blocks.slice(0, -1),
3544
+ launchAttempts: index.launchAttempts.filter((attempt) => attempt.position < last.start)
3545
+ };
3546
+ }
3547
+ async function indexGeneration(handle, identity, size, signal) {
3548
+ signal?.throwIfAborted();
3549
+ const pending = indexing.get(identity);
3550
+ if (pending) {
3551
+ await new Promise((resolve, reject) => {
3552
+ const cancel = () => reject(signal?.reason);
3553
+ signal?.addEventListener("abort", cancel, { once: true });
3554
+ if (signal?.aborted) cancel();
3555
+ pending.then(() => resolve(), () => resolve()).finally(() => signal?.removeEventListener("abort", cancel));
3556
+ });
3557
+ return indexGeneration(handle, identity, size, signal);
3558
+ }
3559
+ const building = buildGenerationIndex(handle, identity, size, signal);
3560
+ indexing.set(identity, building);
3561
+ try {
3562
+ return await building;
3563
+ } finally {
3564
+ indexing.delete(identity);
3565
+ }
3566
+ }
3567
+ async function buildGenerationIndex(handle, identity, size, signal) {
3568
+ signal?.throwIfAborted();
3569
+ const cached = cache.get(identity);
3570
+ const known = cached && await stillExtends(handle, cached, size) ? cached : null;
3571
+ if (known && known.size === size) {
3572
+ cache.delete(identity);
3573
+ cache.set(identity, known);
3574
+ return known;
3575
+ }
3576
+ const index = known ? resumeFrom(known) : emptyIndex();
3577
+ const next = {
3578
+ ...index,
3579
+ markers: new Map(index.markers),
3580
+ launchAttempts: [...index.launchAttempts],
3581
+ slots: new Set(index.slots)
3582
+ };
3583
+ let offset = next.size;
3584
+ let length = BLOCK_BYTES;
3585
+ while (offset < size) {
3586
+ signal?.throwIfAborted();
3587
+ const buffer = await read(handle, offset, Math.min(length, size - offset));
3588
+ const end = buffer.lastIndexOf(NEWLINE);
3589
+ if (end === -1) {
3590
+ if (buffer.length < Math.min(length, size - offset) || offset + buffer.length >= size) break;
3591
+ length *= 2;
3592
+ continue;
3593
+ }
3594
+ next.blocks.push(indexBlock(next, buffer.subarray(0, end + 1), offset));
3595
+ offset += end + 1;
3596
+ length = BLOCK_BYTES;
3597
+ signal?.throwIfAborted();
3598
+ await setImmediate$1();
3599
+ }
3600
+ next.size = offset;
3601
+ next.head = await read(handle, 0, Math.min(VALIDATE_BYTES, offset));
3602
+ next.tail = await read(handle, Math.max(0, offset - VALIDATE_BYTES), Math.min(VALIDATE_BYTES, offset));
3603
+ cache.delete(identity);
3604
+ cache.set(identity, next);
3605
+ while (cache.size > MAX_CACHED_GENERATIONS) cache.delete(cache.keys().next().value);
3606
+ return next;
3607
+ }
3608
+ /**
3609
+ * Opens `dir` for reading. With `order: 'timeline'`, native crash files follow every other file at equal
3610
+ * timestamps, as `readLogTimeline` orders them; with `'name'`, every file is ranked by name, as `readLogRecords`.
3611
+ */
3612
+ async function openLogDir(dir, { order = "timeline", signal } = {}) {
3613
+ signal?.throwIfAborted();
3614
+ let names;
3615
+ try {
3616
+ names = (await readdir(dir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".ndjson")).map((entry) => entry.name).toSorted();
3617
+ } catch {
3618
+ names = [];
3619
+ }
3620
+ const ranked = order === "name" ? names : [...names.filter((name) => !name.startsWith(NATIVE_CRASH_LOG_PREFIX)), ...names.filter((name) => name.startsWith(NATIVE_CRASH_LOG_PREFIX))];
3621
+ const generations = [];
3622
+ const sizes = {};
3623
+ const close = async () => {
3624
+ await Promise.all(generations.map((generation) => generation.handle.close().catch(() => {})));
3625
+ };
3626
+ try {
3627
+ for (const [rank, name] of ranked.entries()) for (const [generation, path] of [rotatedLogPath(join(dir, name)), join(dir, name)].entries()) {
3628
+ let handle;
3629
+ try {
3630
+ handle = await open(path, "r");
3631
+ } catch {
3632
+ continue;
3633
+ }
3634
+ try {
3635
+ const stat = await handle.stat({ bigint: true });
3636
+ const index = await indexGeneration(handle, `${stat.dev}:${stat.ino}`, Number(stat.size), signal);
3637
+ generations.push({
3638
+ handle,
3639
+ rank,
3640
+ generation,
3641
+ crash: name.startsWith(NATIVE_CRASH_LOG_PREFIX),
3642
+ index
3643
+ });
3644
+ if (generation === 1) sizes[name] = index.size;
3645
+ } catch (error) {
3646
+ await handle.close().catch(() => {});
3647
+ throw error;
3648
+ }
3649
+ }
3650
+ } catch (error) {
3651
+ await close();
3652
+ throw error;
3653
+ }
3654
+ let firstTs;
3655
+ for (const { index } of generations) if (index.minTs !== null && (firstTs === void 0 || index.minTs < firstTs)) firstTs = index.minTs;
3656
+ return {
3657
+ sizes,
3658
+ firstTs,
3659
+ launchAttempts: () => generations.filter((generation) => !generation.crash).flatMap(({ index, rank, generation }) => index.launchAttempts.map(({ record, position }) => ({
3660
+ ts: tsKey(record),
3661
+ rank,
3662
+ generation,
3663
+ position,
3664
+ record
3665
+ }))).toSorted(compare).map((entry) => entry.record),
3666
+ query: (query = {}) => queryGenerations(generations, query, signal),
3667
+ batches: (query = {}) => queryBatches(generations, query, signal),
3668
+ filter: (query = {}) => planLogQuery(generationMarkers(generations), query).matches,
3669
+ close
3670
+ };
3671
+ }
3672
+ function blockMatches(block, { errorsOnly, minLevel, sources }) {
3673
+ if (errorsOnly && !block.levels.has("error") && !block.levels.has("fatal")) return false;
3674
+ if (minLevel && ![...block.levels].some((level) => levelRank(level) >= levelRank(minLevel))) return false;
3675
+ return !sources?.length || sources.some((source) => block.sources.has(source)) || Boolean(errorsOnly && block.sources.has("device"));
3676
+ }
3677
+ async function select(generations, extra, { matches, excludesUpTo, mayContain, tail, first = false }, signal) {
3678
+ if (tail !== void 0 && tail <= 0) return [];
3679
+ const heap = new BoundedSelection(compare, tail, first);
3680
+ const extraRank = generations.length ? Math.max(...generations.map((generation) => generation.rank)) + 1 : 0;
3681
+ for (const [position, record] of extra.entries()) {
3682
+ const entry = {
3683
+ ts: tsKey(record),
3684
+ rank: extraRank,
3685
+ generation: 0,
3686
+ position,
3687
+ record
3688
+ };
3689
+ if (matches(record, entry)) heap.keep(entry);
3690
+ }
3691
+ const blocks = generations.flatMap((generation) => generation.index.blocks.map((block) => ({
3692
+ generation,
3693
+ block
3694
+ }))).toSorted((a, b) => (first ? a.block.minTs - b.block.minTs : b.block.maxTs - a.block.maxTs) || 0);
3695
+ for (const { generation, block } of blocks) {
3696
+ if (excludesUpTo(block.maxTs)) {
3697
+ if (first) continue;
3698
+ break;
3699
+ }
3700
+ if (first && tail !== void 0 && heap.size >= tail && block.minTs > heap.peek().ts) break;
3701
+ if (!first && tail !== void 0 && heap.size >= tail && block.maxTs < heap.peek().ts) break;
3702
+ signal?.throwIfAborted();
3703
+ if (!mayContain(block, generation)) continue;
3704
+ const buffer = await read(generation.handle, block.start, block.end - block.start);
3705
+ for (let lineStart = 0; lineStart < buffer.length;) {
3706
+ const lineEnd = buffer.indexOf(NEWLINE, lineStart);
3707
+ if (lineEnd === -1) break;
3708
+ const record = parseNdjsonLine(buffer.toString("utf8", lineStart, lineEnd));
3709
+ const position = block.start + lineStart;
3710
+ lineStart = lineEnd + 1;
3711
+ if (!record) continue;
3712
+ const entry = {
3713
+ ts: tsKey(record),
3714
+ rank: generation.rank,
3715
+ generation: generation.generation,
3716
+ position,
3717
+ record
3718
+ };
3719
+ if (matches(record, entry)) heap.keep(entry);
3720
+ }
3721
+ await setImmediate$1();
3722
+ }
3723
+ return heap.sorted();
3724
+ }
3725
+ function generationMarkers(generations) {
3726
+ const markers = /* @__PURE__ */ new Map();
3727
+ for (const { index } of generations) for (const [key, marker] of index.markers) keepNewer(markers, key, marker);
3728
+ return [...markers.values()];
3729
+ }
3730
+ async function selectQuery(generations, { tail, extra = [], ...options }, signal, page) {
3731
+ signal?.throwIfAborted();
3732
+ const slots = /* @__PURE__ */ new Set(["default"]);
3733
+ for (const { index } of generations) for (const slot of index.slots) slots.add(slot);
3734
+ for (const record of extra) slots.add(record.slot ?? "default");
3735
+ const plan = planLogQuery([...generationMarkers(generations), ...extra.filter((record) => record.marker === true)], options);
3736
+ return select(generations, extra, {
3737
+ matches: (record, entry) => plan.matches(record) && (!page?.after || compare(entry, page.after) > 0),
3738
+ excludesUpTo: (maxTs) => plan.excludesUpTo(maxTs, slots),
3739
+ mayContain: (block, generation) => blockMatches(block, options) && (!page?.after || compare({
3740
+ ts: block.maxTs,
3741
+ rank: generation.rank,
3742
+ generation: generation.generation,
3743
+ position: block.end
3744
+ }, page.after) > 0),
3745
+ tail: page?.size ?? tail,
3746
+ first: page !== void 0
3747
+ }, signal);
3748
+ }
3749
+ async function queryGenerations(generations, query, signal) {
3750
+ return withErrorContext(generations, query, await selectQuery(generations, query, signal), signal);
3751
+ }
3752
+ async function* queryBatches(generations, query, signal) {
3753
+ const fixed = {
3754
+ ...query,
3755
+ now: query.now ?? Date.now()
3756
+ };
3757
+ let after;
3758
+ for (;;) {
3759
+ const selected = await selectQuery(generations, fixed, signal, {
3760
+ after,
3761
+ size: 500
3762
+ });
3763
+ if (!selected.length) return;
3764
+ yield await withErrorContext(generations, fixed, selected, signal);
3765
+ after = selected.at(-1);
3766
+ }
3767
+ }
3768
+ async function withErrorContext(generations, { extra = [], ...options }, selected, signal) {
3769
+ const matched = selected.map((entry) => entry.record);
3770
+ if (!options.errorsOnly) return matched;
3771
+ const errors = matched.filter((record) => record.src === "metro" && record.raw === true && (record.level === "error" || record.level === "fatal") && typeof record.msg === "string");
3772
+ if (!errors.length) return matched;
3773
+ const contextual = /* @__PURE__ */ new Map();
3774
+ for (const error of errors) {
3775
+ const [first] = await select(generations, extra, {
3776
+ matches: (record) => record.ts === error.ts && record.src === error.src && record.msg === error.msg && record.event === error.event,
3777
+ excludesUpTo: (maxTs) => maxTs < tsKey(error),
3778
+ mayContain: (block) => block.sources.has("metro"),
3779
+ tail: 1,
3780
+ first: true
3781
+ }, signal);
3782
+ if (!first) continue;
3783
+ let limit = 32;
3784
+ for (;;) {
3785
+ const stream = (await select(generations, extra, {
3786
+ matches: (record, entry) => compare(entry, first) >= 0 && record.src === "metro" && record.event === error.event && tsKey(record) >= tsKey(error) && (record.raw === true || record.ts === error.ts && record.msg === error.msg),
3787
+ excludesUpTo: (maxTs) => maxTs < tsKey(error),
3788
+ mayContain: (block) => block.sources.has("metro"),
3789
+ tail: limit,
3790
+ first: true
3791
+ }, signal)).map((entry) => entry.record);
3792
+ const index = stream.findIndex((record) => record.ts === error.ts && record.msg === error.msg);
3793
+ if (index < 0 || stream.slice(index + 1).some((record) => record.raw === true && !isExpoErrorContext(record, error.event)) || stream.length < limit) {
3794
+ contextual.set(error, attachExpoErrorContext(stream, [error], "field")[0]);
3795
+ break;
3796
+ }
3797
+ limit *= 2;
3798
+ }
3799
+ }
3800
+ return matched.map((record) => contextual.get(record) ?? record);
3801
+ }
3802
+ //#endregion
2915
3803
  //#region state/recordings.ts
2916
3804
  /** The footage stim-server keeps per device: the last 15 minutes recorded, however long ago. */
2917
3805
  const RECORDING_FOOTAGE_MS = 9e5;
@@ -3042,10 +3930,13 @@ function validRunId(value) {
3042
3930
  }
3043
3931
  let current = null;
3044
3932
  /**
3045
- * The id of this CLI invocation: `STIM_RUN_ID` when it holds a valid id (Stim Desktop passes one), else a random one.
3046
- * The CLI entry exports it back to `STIM_RUN_ID` so processes the run starts inherit it; this function does not.
3933
+ * The run id of the ambient operation context, else of this CLI invocation: `STIM_RUN_ID` when it holds a valid id
3934
+ * (Stim Desktop passes one), else a random one. The CLI entry exports it back to `STIM_RUN_ID` so processes the run
3935
+ * starts inherit it; this function does not.
3047
3936
  */
3048
3937
  function runId(env = process.env) {
3938
+ const context = ambientContext();
3939
+ if (context) return context.runId;
3049
3940
  if (current === null) current = validRunId(env["STIM_RUN_ID"]) ?? randomBytes(6).toString("hex");
3050
3941
  return current;
3051
3942
  }
@@ -3348,12 +4239,16 @@ function readPullRequestCache(path) {
3348
4239
  if (typeof entry.checkedAt !== "string") return null;
3349
4240
  const pullRequest = pullRequestOf(entry.pullRequest);
3350
4241
  if (pullRequest === void 0) return null;
4242
+ const { containsHead, endedAt } = entry;
3351
4243
  return {
3352
4244
  path,
3353
4245
  branch: entry.branch,
3354
4246
  head: entry.head,
3355
4247
  checkedAt: entry.checkedAt,
3356
- pullRequest
4248
+ pullRequest,
4249
+ ...typeof containsHead === "boolean" ? { containsHead } : {},
4250
+ ...endedAt === null || typeof endedAt === "number" && Number.isFinite(endedAt) ? { endedAt } : {},
4251
+ ...entry.partial === true ? { partial: true } : {}
3357
4252
  };
3358
4253
  }
3359
4254
  /** The cached size of the folder at `path`, or null before it was measured. */
@@ -3453,7 +4348,7 @@ function readAgentSessionsCache() {
3453
4348
  //#region state/stim-build.ts
3454
4349
  /**
3455
4350
  * A digest of Stim's built code: the first 16 hex characters of the sha256 over the sorted `*.mjs` names and
3456
- * bytes of a `stim` `dist` directory. A client and a remote Mac offload only when both compute the same one.
4351
+ * bytes of a `stim` `dist` directory. A client and a remote machine offload only when both compute the same one.
3457
4352
  */
3458
4353
  function stimBuildDigest(dir) {
3459
4354
  try {
@@ -3718,7 +4613,22 @@ function lastUseFrom(state, logMtimes) {
3718
4613
  return candidates.length ? Math.max(...candidates) : NaN;
3719
4614
  }
3720
4615
  const MAINTENANCE_LOG_NAME = "maintenance.ndjson";
4616
+ /**
4617
+ * When the workspace was last used, in epoch milliseconds. A workspace that recorded no use falls back to the mtime
4618
+ * of its workspace.json, which is written once when the workspace is created, so it is a lower bound on the last
4619
+ * use. NaN when its state.json exists but does not parse, or when workspace.json cannot be read either.
4620
+ */
3721
4621
  function workspaceLastUsed(root) {
4622
+ const used = recordedLastUse(root);
4623
+ if (Number.isFinite(used)) return used;
4624
+ if (existsSync(workspaceStateFile(root)) && readWorkspaceState(root) === null) return NaN;
4625
+ try {
4626
+ return statSync(workspaceMetadataFile(root)).mtimeMs;
4627
+ } catch {
4628
+ return NaN;
4629
+ }
4630
+ }
4631
+ function recordedLastUse(root) {
3722
4632
  const logs = workspaceLogsDir(root);
3723
4633
  let mtimes = [];
3724
4634
  try {
@@ -3796,7 +4706,7 @@ function readViewedDevices(dir = deviceViewersDir()) {
3796
4706
  //#endregion
3797
4707
  //#region state/stats.ts
3798
4708
  const STATS_VERSION = 1;
3799
- /** Where a compiling build ran: here, on a remote Mac, or here after offloading it failed. */
4709
+ /** Where a compiling build ran: here, on a remote machine, or here after offloading it failed. */
3800
4710
  const PLACEMENT_DECISIONS = [
3801
4711
  "here",
3802
4712
  "offloaded",
@@ -3944,7 +4854,7 @@ function normalize(record) {
3944
4854
  const source = record.projects;
3945
4855
  if (source && typeof source === "object" && !Array.isArray(source)) for (const [key, scope] of Object.entries(source)) projects[key] = normalizeScope(scope);
3946
4856
  const history = {};
3947
- if (isObject(record.history)) for (const [key, scope] of Object.entries(record.history)) {
4857
+ if (isJsonObject(record.history)) for (const [key, scope] of Object.entries(record.history)) {
3948
4858
  const normalized = normalizeHistory(scope);
3949
4859
  if (Object.keys(normalized).length) history[key] = normalized;
3950
4860
  }
@@ -3952,8 +4862,8 @@ function normalize(record) {
3952
4862
  const capacityRefusals = Array.isArray(record.capacityRefusals) ? record.capacityRefusals.flatMap(normalizeCapacityRefusal) : [];
3953
4863
  const capacityWaits = Array.isArray(record.capacityWaits) ? record.capacityWaits.flatMap(normalizeCapacityWait) : [];
3954
4864
  const buildMachines = {};
3955
- if (isObject(record.buildMachines)) {
3956
- for (const [name, totals] of Object.entries(record.buildMachines)) if (isObject(totals)) buildMachines[name] = normalizeMachineTotals(totals);
4865
+ if (isJsonObject(record.buildMachines)) {
4866
+ for (const [name, totals] of Object.entries(record.buildMachines)) if (isJsonObject(totals)) buildMachines[name] = normalizeMachineTotals(totals);
3957
4867
  }
3958
4868
  return {
3959
4869
  version: 1,
@@ -3967,7 +4877,7 @@ function normalize(record) {
3967
4877
  };
3968
4878
  }
3969
4879
  function normalizePlacement(value) {
3970
- if (!isObject(value)) return [];
4880
+ if (!isJsonObject(value)) return [];
3971
4881
  const { decision, platform, reason, project, machine } = value;
3972
4882
  if (!PLACEMENT_DECISIONS.includes(decision) || !PLATFORMS.includes(platform)) return [];
3973
4883
  if (typeof reason !== "string" || typeof project !== "string") return [];
@@ -3990,7 +4900,7 @@ function normalizePlacement(value) {
3990
4900
  }];
3991
4901
  }
3992
4902
  function normalizeCapacityRefusal(value) {
3993
- if (!isObject(value)) return [];
4903
+ if (!isJsonObject(value)) return [];
3994
4904
  const { at, kind, platform, max, workspace } = value;
3995
4905
  if (kind !== "device" || !PLATFORMS.includes(platform)) return [];
3996
4906
  if (typeof at !== "string" || !Number.isFinite(Date.parse(at))) return [];
@@ -4005,7 +4915,7 @@ function normalizeCapacityRefusal(value) {
4005
4915
  }];
4006
4916
  }
4007
4917
  function normalizeCapacityWait(value) {
4008
- if (!isObject(value) || value.kind !== "device-wait") return [];
4918
+ if (!isJsonObject(value) || value.kind !== "device-wait") return [];
4009
4919
  const refusal = normalizeCapacityRefusal({
4010
4920
  ...value,
4011
4921
  kind: "device"
@@ -4029,20 +4939,17 @@ function normalizeMachineTotals(totals) {
4029
4939
  ...typeof totals.lastFallbackAt === "string" ? { lastFallbackAt: totals.lastFallbackAt } : {}
4030
4940
  };
4031
4941
  }
4032
- function isObject(value) {
4033
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4034
- }
4035
4942
  function normalizeHistory(scope) {
4036
4943
  const history = {};
4037
- if (!isObject(scope)) return history;
4944
+ if (!isJsonObject(scope)) return history;
4038
4945
  for (const platform of PLATFORMS) {
4039
4946
  const lists = scope[platform];
4040
- if (!isObject(lists)) continue;
4947
+ if (!isJsonObject(lists)) continue;
4041
4948
  const normalized = {};
4042
4949
  for (const outcome of OUTCOMES) {
4043
4950
  const samples = lists[outcome];
4044
4951
  if (!Array.isArray(samples)) continue;
4045
- const kept = samples.filter(isObject).flatMap((sample) => {
4952
+ const kept = samples.filter(isJsonObject).flatMap((sample) => {
4046
4953
  const durationMs = wholeMs(sample.durationMs);
4047
4954
  if (durationMs <= 0) return [];
4048
4955
  return [{
@@ -4060,7 +4967,7 @@ function normalizeHistory(scope) {
4060
4967
  }
4061
4968
  function wholePhases(phases) {
4062
4969
  const out = {};
4063
- if (!isObject(phases)) return out;
4970
+ if (!isJsonObject(phases)) return out;
4064
4971
  for (const [name, ms] of Object.entries(phases)) {
4065
4972
  const value = Number(ms);
4066
4973
  if (Number.isFinite(value) && value >= 0) out[name] = Math.round(value);
@@ -4187,6 +5094,8 @@ const DEVICE_HOST_LIMITS = {
4187
5094
  logsMs: 15e3,
4188
5095
  killGraceMs: 5e3
4189
5096
  };
5097
+ /** How many parked sessions one reserve inspects, and possibly retires, before it creates a device. */
5098
+ const HOSTED_PARKED_CANDIDATES = 3;
4190
5099
  /** How long a cancelled hosted worker run can take to settle: SIGTERM, SIGKILL after the grace, then it is abandoned. */
4191
5100
  function hostedWorkerSettleMs(limits = DEVICE_HOST_LIMITS) {
4192
5101
  return limits.killGraceMs * 2;
@@ -4845,7 +5754,14 @@ const KINDS = [
4845
5754
  "would-stop-workspace",
4846
5755
  "would-remove-worktree",
4847
5756
  "would-remove-orphan",
4848
- "would-unregister-cache"
5757
+ "would-unregister-cache",
5758
+ "would-delete-parked-device",
5759
+ "would-delete-devices",
5760
+ "would-clear-build-dirs",
5761
+ "would-delete-dependencies",
5762
+ "would-stop-gradle-daemons",
5763
+ "would-delete-gradle-version",
5764
+ "would-delete-temp-leftovers"
4849
5765
  ];
4850
5766
  /** Rejects missing, corrupt or unsupported payloads without throwing. */
4851
5767
  function parseMaintenanceState(value) {
@@ -4859,13 +5775,14 @@ function parseMaintenanceState(value) {
4859
5775
  "metro-cache",
4860
5776
  "compilation-cache",
4861
5777
  "ccache",
5778
+ "owned-devices",
4862
5779
  "other"
4863
5780
  ].includes(String(size.category)) && (size.workspace === void 0 || typeof size.workspace === "string") && (size.blocked === void 0 || typeof size.blocked === "string") && (size.idleDays === void 0 || nullableNumber(size.idleDays)))) return null;
4864
5781
  if (!Array.isArray(value.plan) || !value.plan.every((action) => isJsonObject(action) && KINDS.includes(String(action.kind)) && typeof action.target === "string" && finite(action.bytes) && typeof action.reason === "string" && (action.workspace === void 0 || typeof action.workspace === "string") && (action.dir === void 0 || typeof action.dir === "string") && (action.olderThanDays === void 0 || finite(action.olderThanDays)) && (action.check === void 0 || [
4865
5782
  "worktree",
4866
5783
  "sweep",
4867
5784
  "disk"
4868
- ].includes(String(action.check))))) return null;
5785
+ ].includes(String(action.check))) && (action.level === void 0 || action.level === 2 || action.level === 3))) return null;
4869
5786
  if (!Array.isArray(value.recent) || !value.recent.every((record) => isJsonObject(record) && record.src === "maintenance" && finite(record.ts) && typeof record.msg === "string" && [
4870
5787
  "debug",
4871
5788
  "info",
@@ -4881,7 +5798,12 @@ function parseMaintenanceState(value) {
4881
5798
  if (value.lastPass !== null && (!isJsonObject(value.lastPass) || !finite(value.lastPass.startedAt) || !finite(value.lastPass.durationMs) || typeof value.lastPass.trigger !== "string" || value.lastPass.mode !== "report" && value.lastPass.mode !== "on" || !finite(value.lastPass.freedBytes) || value.lastPass.stopped !== 0 || !finite(value.lastPass.actions) || !strings(value.lastPass.blocked))) return null;
4882
5799
  if (value.pressure !== null) {
4883
5800
  const pressure = value.pressure;
4884
- if (!isJsonObject(pressure) || !Array.isArray(pressure.disk) || !pressure.disk.every((disk) => isJsonObject(disk) && typeof disk.volume === "string" && finite(disk.freeMb)) || !nullableNumber(pressure.warningSince) || !isJsonObject(pressure.memory) || ![
5801
+ if (!isJsonObject(pressure) || !Array.isArray(pressure.disk) || !pressure.disk.every((disk) => isJsonObject(disk) && typeof disk.volume === "string" && finite(disk.freeMb)) || !nullableNumber(pressure.warningSince) || pressure.diskLevel !== void 0 && ![
5802
+ 0,
5803
+ 1,
5804
+ 2,
5805
+ 3
5806
+ ].some((level) => level === pressure.diskLevel) || !isJsonObject(pressure.memory) || ![
4885
5807
  null,
4886
5808
  "normal",
4887
5809
  "warning",
@@ -4899,8 +5821,8 @@ function readMaintenanceAttempt() {
4899
5821
  }
4900
5822
  //#endregion
4901
5823
  //#region state/agent-device.ts
4902
- function agentDeviceStateDirs(home = homedir()) {
4903
- const override = process.env.AGENT_DEVICE_STATE_DIR?.trim();
5824
+ function agentDeviceStateDirs(home = ambientEnv().HOME || ambientEnv().USERPROFILE || homedir(), env = ambientEnv()) {
5825
+ const override = env.AGENT_DEVICE_STATE_DIR?.trim();
4904
5826
  const dirs = /* @__PURE__ */ new Set([override ? resolve(override) : join(home, ".agent-device")]);
4905
5827
  const workspaces = join(configDir(), "workspaces");
4906
5828
  try {
@@ -4913,6 +5835,20 @@ function agentDeviceStateDirs(home = homedir()) {
4913
5835
  } catch {}
4914
5836
  return [...dirs];
4915
5837
  }
5838
+ async function agentDeviceStateDirsAsync(home = ambientEnv().HOME || ambientEnv().USERPROFILE || homedir(), env = ambientEnv()) {
5839
+ const override = env.AGENT_DEVICE_STATE_DIR?.trim();
5840
+ const dirs = /* @__PURE__ */ new Set([override ? resolve(override) : join(home, ".agent-device")]);
5841
+ const workspaces = join(configDir(), "workspaces");
5842
+ try {
5843
+ for (const name of await readdir(workspaces)) {
5844
+ const dir = join(workspaces, name, "agent-device");
5845
+ try {
5846
+ if ((await stat(dir)).isDirectory()) dirs.add(dir);
5847
+ } catch {}
5848
+ }
5849
+ } catch {}
5850
+ return [...dirs];
5851
+ }
4916
5852
  //#endregion
4917
5853
  //#region state/archive.ts
4918
5854
  function readArchives() {
@@ -5024,14 +5960,13 @@ const knownKeys = (value, keys) => Object.keys(value).every((key) => keys.includ
5024
5960
  function parseSetupJournal(value) {
5025
5961
  if (!isJsonObject(value) || !knownKeys(value, [
5026
5962
  "v",
5027
- "client",
5028
5963
  "expiresAt",
5029
5964
  "capabilities",
5030
5965
  "steps",
5031
5966
  "granted",
5032
5967
  "done",
5033
5968
  "exit"
5034
- ]) || value.v !== 1 || !isJsonObject(value.client) || !knownKeys(value.client, ["nodeId"]) || !text(value.client.nodeId) || typeof value.expiresAt !== "string" || !Number.isFinite(Date.parse(value.expiresAt)) || !Array.isArray(value.capabilities) || !value.capabilities.every(capability) || !Array.isArray(value.steps) || !value.steps.every((step) => isJsonObject(step) && knownKeys(step, [
5969
+ ]) || value.v !== 1 || typeof value.expiresAt !== "string" || !Number.isFinite(Date.parse(value.expiresAt)) || !Array.isArray(value.capabilities) || !value.capabilities.every(capability) || !Array.isArray(value.steps) || !value.steps.every((step) => isJsonObject(step) && knownKeys(step, [
5035
5970
  "id",
5036
5971
  "state",
5037
5972
  "title",
@@ -5057,4 +5992,4 @@ function isSetupJournalPrunable(journal, now) {
5057
5992
  return Date.parse(journal.expiresAt) + 36e5 < now;
5058
5993
  }
5059
5994
  //#endregion
5060
- 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, DEBUG_ENV, DEVICE_HOST_LIMITS, 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, MAINTENANCE_LOG_NAME, 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, RUN_ID_ENV, 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, automaticMachineEnabled, buildCriteria, buildDiagnostics, buildMachinesFile, buildMachinesLock, buildSlotPath, buildSlotsDir, buildWorkerRoot, bundleMarkerPlatform, capDrops, closedSegmentName, coerceSettingText, compileGrep, configCorruptRepair, configLockPath, countErrorEntries, createAgentActionReader, createDebugLog, createdDevicesFile, createdDevicesLock, debugLogDir, debugLoggingEnabled, decodeStats, deviceHostArea, deviceHostMachinesClaims, deviceHostMachinesFile, deviceHostMachinesLock, deviceHostRoot, deviceViewersDir, diskUsageCacheDir, diskUsageCacheFile, diskUsageFailureFile, diskUsageWalkClaims, 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, hostedWorkerSettleMs, 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, projectRootDirectories, pullRequestCacheDir, pullRequestCacheFile, queryJsonLogs, queryLogs, readAgentDeviceUsage, readAgentDeviceUsageCache, readAgentSessionsCache, readArchive, readArchiveState, readArchives, readBuildDetail, readBuildHistory, readBuildMachines, readCreatedDevices, readDevServerStop, readDevServerStopRequest, readDeviceHostMachines, readDeviceIdleShutdowns, readDevicePlacement, readDiskUsage, readDiskUsageFailure, 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, requireAutomaticMachine, runId, 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, validRunId, validateAutomaticPool, wholePhases, withConfigLock, withDerivedFacts, withGitChip, withStateReadCache, workspaceAgentDeviceDir, workspaceBuildDetailFile, workspaceDerivedData, workspaceEndedAgentsFile, workspaceEndedAgentsLock, workspaceGradleBuild, workspaceLastUsed, workspaceLogErrorIndex, workspaceLogsDir, workspaceMetadataFile, workspaceRecordingsDir, workspaceStateFile, workspaceStateLock };
5995
+ 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, DEBUG_ENV, DEVICE_HOST_LIMITS, 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, HOSTED_PARKED_CANDIDATES, IDLE_STOP_KEY, IOS_SIMULATOR_APPS, KeyedEntities, LAST_BUILD_KEYS, LEVELS, MACHINE_CREDENTIALS_VERSION, MACHINE_OWNER_KINDS, MAINTENANCE_LOG_NAME, 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, RUN_ID_ENV, SETTINGS, SETTINGS_SCHEMA_FILE, SETTINGS_SCHEMA_URL, SETTING_GROUPS, SETTING_SCOPES, SOURCES, STATS_VERSION, STATUS_ISSUE_CODES, SUMMARY_ARCHIVES, 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, agentDeviceStateDirsAsync, agentDeviceUsageFile, agentSessionOf, agentSessionsCacheFile, allMetroPorts, applyKeyedDelta, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, assertHostedDeviceLedger, attachExpoErrorContext, automaticMachineEnabled, buildCriteria, buildDiagnostics, buildMachinesFile, buildMachinesLock, buildSlotPath, buildSlotsDir, buildWorkerRoot, bundleMarkerPlatform, cannotHost, capDrops, clientIdentity, clientIdentityKeyFile, closedSegmentName, coerceSettingText, compileGrep, configCorruptRepair, configLockPath, countErrorEntries, createAgentActionReader, createAsyncAgentActionReader, createDebugLog, createdDevicesFile, createdDevicesLock, debugLogDir, debugLoggingEnabled, decodeStats, deviceHostArea, deviceHostMachinesClaims, deviceHostMachinesFile, deviceHostMachinesLock, deviceHostRoot, deviceViewersDir, diskUsageCacheDir, diskUsageCacheFile, diskUsageFailureFile, diskUsageWalkClaims, easMachineStateRoot, easSessionLedgerFile, easSessionLedgerLock, emulatorLogFile, endedAgentSessionOf, endpoint, expectedShape, fileSizes, findEnclosingWorktreeRoot, findPeer, findProjectRoot, followLogs, footageDrops, getConcurrencyLimits, getConfigPath, getParkedMax, getProject, getProjectSettings, getRepoSettings, gitMergeCacheDir, hostedAgentDriverName, hostedAndroidDeviceName, hostedAndroidPlacements, hostedAndroidRecords, hostedAndroidStatus, hostedAppArea, hostedAppAttempt, hostedAppBlobs, hostedConsolePort, hostedDeviceId, hostedIosPlacements, hostedIosRecords, hostedIosStatus, hostedMacosAppSlot, hostedMacosBundleId, hostedMacosLogsDir, hostedMetroGateways, hostedMetroRequests, hostedNativeLogsDir, hostedNativeRecords, hostedWorkerSettleMs, isExpoErrorContext, isJournalExpired, isJsonObject, isLayeredSetting, isPathPrefix, isSensitiveReference, isSetupJournalPrunable, lastUseFrom, levelRank, linkReplacedArchives, listRecordedDevices, listSegments, listStatusCacheEntries, loadConfig, logFiles, machineCacheUsageFile, machineCapacity, machineEndpoint, macosAppState, maintenanceAttemptFile, maintenanceChildLogFile, maintenanceDir, maintenanceNdjsonFile, maintenanceRunClaims, maintenanceStateFile, markerWindow, offloadSummary, openLogDir, openSegmentName, parseHostedAgentAccess, parseHostedAgentGrant, parseHostedAndroidDevice, parseHostedAndroidPlacement, parseHostedAppManifest, parseHostedAppOffer, parseHostedChoice, parseHostedDevice, parseHostedIosPlacement, parseHostedLocalDeviceCount, parseHostedLogsCursor, parseHostedMacosChoice, parseHostedMacosDevice, parseHostedMacosPlacement, parseHostedNativeOffer, parseHostedNativePlacement, parseHostedOfferRequest, parseHostedPlatformDevice, parseHostedRequest, parseMachine, parseMachineCredential, parseMacosRecord, parseMaintenanceState, parseMissReason, parseNdjsonLine, parseNdjsonText, parseSetupJournal, parseSince, planLogQuery, projectRootDirectories, pullRequestCacheDir, pullRequestCacheFile, queryJsonLogs, queryLogs, readAgentDeviceUsage, readAgentDeviceUsageCache, readAgentSessionsCache, readArchive, readArchiveState, readArchives, readBuildDetail, readBuildHistory, readBuildMachines, readCreatedDevices, readDevServerStop, readDevServerStopRequest, readDeviceHostMachines, readDeviceIdleShutdowns, readDevicePlacement, readDiskUsage, readDiskUsageFailure, readEasSessionLedger, readEndedAgentSessions, readHomeIdentity, readHostedApp, readHostedAppMetadata, readHostedDevice, readHostedDeviceLedger, readHostedMacosApp, readHostedNativeLogsCheckpoint, readHostedSessions, readIdleStop, readJsonFile, readJsonObject, readLastBuilds, readLogRecords, readLogTimeline, readLogsSince, readMachineCacheUsage, readMacosRecord, readMaintenanceAttempt, readMaintenanceState, readNdjsonGenerations, readPullRequestCache, readRunEstimates, readSetupJournal, readStateFile, readStats, readStatsReport, readSwiftpmCacheUsage, readViewedDevices, readWarmRecord, readWorkspaceAgent, readWorkspaceState, recordMatches, recordedBytes, recordingDeviceName, recordingEnabled, rememberHostedAppBlobRename, requireAutomaticMachine, runId, saturation, serverLocalSocket, serverSetupDir, settingDefinition, settingValueError, settingsJsonSchema, setupJournalFile, sharedCcache, sharedCompilationCache, sharedGradle, sharedPods, sortByTs, statsFile, statsProjectKey, statusViews, stimBuildDigest, supervisorLogFile, supervisorPidFile, swiftpmCacheUsageFile, tailRead, trimCapacityRefusals, trimCapacityWaits, trimSamples, tryAcquireBuildSlotClaim, unreadableHostedAndroid, unreadableHostedIos, unreadableHostedNative, validEasSessionClaim, validHostedAppArguments, validHostedAppBlob, validRunId, validateAutomaticPool, wholePhases, windowsShortDir, windowsShortDirKey, withConfigLock, withConfigLockAsync, withDerivedFacts, withGitChip, withStateReadCache, workspaceAgentDeviceDir, workspaceBuildDetailFile, workspaceDerivedData, workspaceEndedAgentsFile, workspaceEndedAgentsLock, workspaceGradleBuild, workspaceLastUsed, workspaceLogErrorIndex, workspaceLogsDir, workspaceMetadataFile, workspaceRecordingsDir, workspaceStateFile, workspaceStateLock, workspaceSummary };