@stim-cli/core 1.20.2 → 1.22.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.
Files changed (34) hide show
  1. package/README.md +2 -0
  2. package/dist/{core-BkgfKqJ8.mjs → core-BTsCO1nW.mjs} +444 -60
  3. package/dist/{index-VDWBflH3.d.mts → index-DKq1j8dw.d.mts} +340 -91
  4. package/dist/index.d.mts +101 -5
  5. package/dist/index.mjs +3 -3
  6. package/dist/keyed-delta-BBoOeR2J.mjs +289 -0
  7. package/dist/noise-DYIbymAJ.d.mts +100 -0
  8. package/dist/noise-session.d.mts +164 -0
  9. package/dist/noise-session.mjs +402 -0
  10. package/dist/noise.d.mts +2 -0
  11. package/dist/noise.mjs +393 -0
  12. package/dist/oversight.mjs +16 -6
  13. package/dist/{ownership-claim-O31Y5iIb.mjs → ownership-claim-DHUs_Dir.mjs} +1 -1
  14. package/dist/{ownership-claim-Xxxr9MPp.d.mts → ownership-claim-RY3fbm1D.d.mts} +3 -2
  15. package/dist/ownership-claim.d.mts +2 -2
  16. package/dist/ownership-claim.mjs +2 -2
  17. package/dist/phone-protocol.d.mts +85 -16
  18. package/dist/phone-protocol.mjs +2 -2
  19. package/dist/{process-identity-BTyww2Xj.d.mts → process-identity-DVNGpF5c.d.mts} +1 -1
  20. package/dist/process-identity.d.mts +1 -1
  21. package/dist/process-identity.mjs +3 -1
  22. package/dist/{protocol-pH632kA3.d.mts → protocol-BjJt4ghB.d.mts} +632 -65
  23. package/dist/protocol.d.mts +2 -2
  24. package/dist/protocol.mjs +81 -10
  25. package/dist/receive-protocol.d.mts +1 -1
  26. package/dist/receive-protocol.mjs +20163 -14469
  27. package/dist/state.d.mts +2 -2
  28. package/dist/state.mjs +1261 -208
  29. package/dist/toolchain.d.mts +64 -0
  30. package/dist/toolchain.mjs +326 -0
  31. package/dist/workspace-link.d.mts +16 -0
  32. package/dist/workspace-link.mjs +50 -0
  33. package/package.json +21 -1
  34. package/dist/status-derived-C13g6eQt.mjs +0 -151
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, H as ambientContext, J as readJsonObject, K as isJsonObject, Q as withDirLockAsync, T as workspaceStateDir, U as ambientEnv, X as withStateReadCache, Y as readStateFile, Z as withDirLock, _ as rotatedLogPath, d as ensurePrivateDir, g as rotateLog, q as readJsonFile, t as LOG_ROTATE_BYTES, u as configDir } from "./core-BTsCO1nW.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 { lstat, 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",
@@ -1247,6 +1538,12 @@ const SETTINGS = [
1247
1538
  scopes: PROJECT,
1248
1539
  description: "Development Info.plist relative to the Swift Package directory."
1249
1540
  },
1541
+ {
1542
+ key: "macos.artifactInputs",
1543
+ type: OBJECT,
1544
+ scopes: PROJECT,
1545
+ description: "Complete pinned, repeatable SwiftPM inputs producing a relocatable app for completed artifact caching."
1546
+ },
1250
1547
  {
1251
1548
  key: "macos.arguments",
1252
1549
  type: { kind: "strings" },
@@ -1647,6 +1944,18 @@ const SETTINGS = [
1647
1944
  env: "STIM_MAINTENANCE_OLDER_THAN_DAYS",
1648
1945
  description: "Age in days for the age sweep: unused workspace build outputs and shared cache entries"
1649
1946
  },
1947
+ {
1948
+ key: "maintenance.deviceIdleDays",
1949
+ type: {
1950
+ kind: "number",
1951
+ integer: true,
1952
+ minimum: 0
1953
+ },
1954
+ scopes: MACHINE,
1955
+ default: 3,
1956
+ env: "STIM_MAINTENANCE_DEVICE_IDLE_DAYS",
1957
+ description: "Days a workspace stays idle before the age sweep deletes its owned simulators and emulators; 0 turns that off"
1958
+ },
1650
1959
  {
1651
1960
  key: "maintenance.protectRecentHours",
1652
1961
  type: {
@@ -1659,20 +1968,60 @@ const SETTINGS = [
1659
1968
  env: "STIM_MAINTENANCE_PROTECT_RECENT_HOURS",
1660
1969
  description: "Cache entries used within this many hours are never evicted"
1661
1970
  },
1971
+ {
1972
+ key: "maintenance.urgentFreeDiskGb",
1973
+ type: GIGABYTES,
1974
+ scopes: MACHINE,
1975
+ default: 12,
1976
+ env: "STIM_MAINTENANCE_URGENT_FREE_DISK_GB",
1977
+ description: "Free disk, in GB, below which automatic maintenance reports disk pressure level 2; 0 turns that threshold off"
1978
+ },
1979
+ {
1980
+ key: "maintenance.criticalFreeDiskGb",
1981
+ type: GIGABYTES,
1982
+ scopes: MACHINE,
1983
+ default: 6,
1984
+ env: "STIM_MAINTENANCE_CRITICAL_FREE_DISK_GB",
1985
+ 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"
1986
+ },
1987
+ {
1988
+ key: "maintenance.pressureProtectMinutes",
1989
+ type: {
1990
+ kind: "number",
1991
+ integer: true,
1992
+ minimum: 0
1993
+ },
1994
+ scopes: MACHINE,
1995
+ default: 10,
1996
+ env: "STIM_MAINTENANCE_PRESSURE_PROTECT_MINUTES",
1997
+ description: "Protection window, in minutes, that replaces maintenance.protectRecentHours at disk pressure levels 2 and 3"
1998
+ },
1999
+ {
2000
+ key: "maintenance.gradleVersionIdleDays",
2001
+ type: {
2002
+ kind: "number",
2003
+ integer: true,
2004
+ minimum: 0
2005
+ },
2006
+ scopes: MACHINE,
2007
+ default: 7,
2008
+ env: "STIM_MAINTENANCE_GRADLE_VERSION_IDLE_DAYS",
2009
+ 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"
2010
+ },
1662
2011
  {
1663
2012
  key: "maintenance.removeFinishedWorktrees",
1664
2013
  type: BOOLEAN,
1665
2014
  scopes: MACHINE,
1666
2015
  default: true,
1667
2016
  env: "STIM_MAINTENANCE_REMOVE_FINISHED_WORKTREES",
1668
- description: "Remove clean linked worktrees whose branch or pull request finished, under gc.worktreeGraceMinutes"
2017
+ 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
2018
  },
1670
2019
  {
1671
2020
  key: "maintenance.keep",
1672
2021
  type: BOOLEAN,
1673
2022
  scopes: PROJECT,
1674
2023
  default: false,
1675
- description: "Pin this workspace: automatic maintenance never clears its build outputs or removes its worktree"
2024
+ description: "Pin this workspace: automatic maintenance never clears its build outputs, deletes its owned devices or removes its worktree"
1676
2025
  },
1677
2026
  {
1678
2027
  key: "maintenance.workspaceOutputsMaxGb",
@@ -1761,7 +2110,7 @@ const SETTINGS = [
1761
2110
  scopes: MACHINE,
1762
2111
  default: false,
1763
2112
  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"
2113
+ 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
2114
  },
1766
2115
  {
1767
2116
  key: "hosting.agentDriver",
@@ -1773,18 +2122,32 @@ const SETTINGS = [
1773
2122
  default: "none",
1774
2123
  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
2124
  },
2125
+ {
2126
+ key: "server.buildsPaused",
2127
+ type: { kind: "boolean" },
2128
+ scopes: MACHINE,
2129
+ default: false,
2130
+ description: "Temporarily refuse new incoming builds without interrupting running builds, serving cached artifacts, or changing client approvals; local CLI builds remain available"
2131
+ },
2132
+ {
2133
+ key: "server.devicesPaused",
2134
+ type: { kind: "boolean" },
2135
+ scopes: MACHINE,
2136
+ default: false,
2137
+ description: "Temporarily refuse new hosted iOS, Android and macOS sessions without interrupting existing sessions or changing client approvals; local CLI devices remain available"
2138
+ },
1776
2139
  {
1777
2140
  key: "server.acceptClientBuilds",
1778
2141
  type: { kind: "boolean" },
1779
2142
  scopes: MACHINE,
1780
2143
  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"
2144
+ 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
2145
  },
1783
2146
  {
1784
2147
  key: "remote.machines",
1785
2148
  type: { kind: "strings" },
1786
2149
  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"
2150
+ 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
2151
  },
1789
2152
  {
1790
2153
  key: "remote.buildPoolDisabled",
@@ -1805,7 +2168,7 @@ const SETTINGS = [
1805
2168
  type: BOOLEAN,
1806
2169
  scopes: EVERY,
1807
2170
  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"
2171
+ 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
2172
  },
1810
2173
  {
1811
2174
  key: "remote.build",
@@ -1817,7 +2180,7 @@ const SETTINGS = [
1817
2180
  scopes: MACHINE,
1818
2181
  default: "auto",
1819
2182
  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"
2183
+ 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
2184
  },
1822
2185
  {
1823
2186
  key: "remote.buildMode",
@@ -1828,7 +2191,7 @@ const SETTINGS = [
1828
2191
  scopes: MACHINE,
1829
2192
  default: "auto",
1830
2193
  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"
2194
+ 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
2195
  },
1833
2196
  {
1834
2197
  key: "server.maxLoadPerCore",
@@ -1838,13 +2201,13 @@ const SETTINGS = [
1838
2201
  },
1839
2202
  scopes: MACHINE,
1840
2203
  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"
2204
+ 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
2205
  },
1843
2206
  {
1844
2207
  key: "server.workerRoot",
1845
2208
  type: ABSOLUTE_PATH,
1846
2209
  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"
2210
+ description: "Directory where stim-server keeps the checkouts, dependencies and build state of the machines that build here; default $STIM_HOME/build-worker"
1848
2211
  },
1849
2212
  {
1850
2213
  key: "server.gradleDaemonIdleMinutes",
@@ -1856,7 +2219,7 @@ const SETTINGS = [
1856
2219
  },
1857
2220
  scopes: MACHINE,
1858
2221
  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"
2222
+ 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
2223
  },
1861
2224
  {
1862
2225
  key: "caches.buildCache",
@@ -1888,9 +2251,6 @@ function isLayeredSetting(setting) {
1888
2251
  return setting.scopes.some((scope) => scope !== "machine");
1889
2252
  }
1890
2253
  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
2254
  function bundleUrlAccepts(value, key) {
1895
2255
  if (typeof value !== "string" || !/^(https?:\/\/|\/(?!\/))/.test(value) || /[\s\\#]/.test(value)) return false;
1896
2256
  try {
@@ -1921,7 +2281,7 @@ function acceptsShape(setting, value) {
1921
2281
  case "strings": return Array.isArray(value) && value.every((entry) => typeof entry === "string");
1922
2282
  case "number": return typeof value === "number";
1923
2283
  case "choice": return typeof value === "string" && type.choices.includes(value);
1924
- case "object": return isPlainObject(value);
2284
+ case "object": return isJsonObject(value);
1925
2285
  case "bundle-url": return bundleUrlAccepts(value, setting.key);
1926
2286
  }
1927
2287
  }
@@ -2026,7 +2386,7 @@ function tryAcquireBuildSlotClaim({ max, details }) {
2026
2386
  if (refusal && !busy) throw refusal;
2027
2387
  return null;
2028
2388
  }
2029
- /** Build slots held for a remote Mac's offloaded builds, which run under another Stim home. */
2389
+ /** Build slots held for a remote machine's offloaded builds, which run under another Stim home. */
2030
2390
  function liveOffloadedSlots() {
2031
2391
  let names;
2032
2392
  try {
@@ -2087,7 +2447,7 @@ function machineCapacity() {
2087
2447
  maxLoadPerCore: maxLoadPerCore()
2088
2448
  };
2089
2449
  }
2090
- /** Why a Mac with this capacity should not take another native build; null when it can. */
2450
+ /** Why a machine with this capacity should not take another native build; null when it can. */
2091
2451
  function saturation(capacity) {
2092
2452
  if (capacity.maxBuilds > 0 && capacity.builds >= capacity.maxBuilds) return `all ${capacity.maxBuilds} build slots busy`;
2093
2453
  if (capacity.loadPerCore >= capacity.maxLoadPerCore) return `load at or above ${capacity.maxLoadPerCore}/core`;
@@ -2095,41 +2455,55 @@ function saturation(capacity) {
2095
2455
  }
2096
2456
  //#endregion
2097
2457
  //#region state/build-machines.ts
2098
- function parseCredential(value) {
2458
+ const MACHINE_CREDENTIALS_VERSION = 2;
2459
+ const KEY = /^[A-Za-z0-9_-]{43}$/;
2460
+ /** A stored credential, or null when it is malformed or from an older version. */
2461
+ function parseMachineCredential(value) {
2099
2462
  if (!isJsonObject(value)) return null;
2100
- const { machine, nodeId, dnsName, deviceId, deviceToken, state, requestedAt } = value;
2463
+ const { machine, transport, address, state, requestedAt, serverKey, deviceId, ticket, expiresAt } = value;
2101
2464
  if (![
2102
2465
  machine,
2103
- nodeId,
2104
- dnsName,
2105
- deviceId,
2106
- deviceToken,
2466
+ address,
2107
2467
  requestedAt
2108
2468
  ].every((field) => typeof field === "string" && field)) return null;
2109
- if (state !== "pending" && state !== "approved") return null;
2469
+ if (transport !== "tailscale" || state !== "pending" && state !== "approved") return null;
2470
+ if (serverKey !== void 0 && (typeof serverKey !== "string" || !KEY.test(serverKey))) return null;
2471
+ if (ticket !== void 0 && (typeof ticket !== "string" || !KEY.test(ticket))) return null;
2472
+ if (state === "approved" && (serverKey === void 0 || typeof deviceId !== "string" || !deviceId)) return null;
2473
+ if (state === "pending" && ticket === void 0) return null;
2110
2474
  return {
2111
2475
  machine,
2112
- nodeId,
2113
- dnsName,
2114
- deviceId,
2115
- deviceToken,
2476
+ transport,
2477
+ address,
2116
2478
  state,
2117
2479
  requestedAt,
2118
- ...typeof value.expiresAt === "string" && value.expiresAt ? { expiresAt: value.expiresAt } : {},
2119
- ...typeof value.ticketHash === "string" ? { ticketHash: value.ticketHash } : {}
2480
+ ...serverKey ? { serverKey } : {},
2481
+ ...typeof deviceId === "string" && deviceId ? { deviceId } : {},
2482
+ ...ticket ? { ticket } : {},
2483
+ ...typeof expiresAt === "string" && expiresAt ? { expiresAt } : {}
2120
2484
  };
2121
2485
  }
2122
2486
  function readBuildMachines() {
2123
2487
  try {
2124
- const machines = readJsonFile(buildMachinesFile()).machines;
2125
- return Array.isArray(machines) ? machines.flatMap((entry) => parseCredential(entry) ?? []) : [];
2488
+ const store = readJsonFile(buildMachinesFile());
2489
+ if (store.version !== 2 || !Array.isArray(store.machines)) return [];
2490
+ return store.machines.flatMap((entry) => parseMachineCredential(entry) ?? []);
2126
2491
  } catch {
2127
2492
  return [];
2128
2493
  }
2129
2494
  }
2130
2495
  //#endregion
2496
+ //#region state/client-identity.ts
2497
+ /** This machine's long-term X25519 key as a client of other machines, in `$STIM_HOME/client-identity.key`. */
2498
+ function clientIdentity() {
2499
+ return readOrCreateKeyFile(clientIdentityKeyFile());
2500
+ }
2501
+ //#endregion
2131
2502
  //#region state/device-host-machines.ts
2132
- /** An absent store is empty; unreadable or malformed credentials refuse access without discarding pins. */
2503
+ /**
2504
+ * An absent store, or one from protocol 1, is empty; an unreadable or malformed one refuses access without discarding
2505
+ * pinned keys.
2506
+ */
2133
2507
  function readDeviceHostMachines() {
2134
2508
  let value;
2135
2509
  try {
@@ -2138,23 +2512,13 @@ function readDeviceHostMachines() {
2138
2512
  if (error.code === "ENOENT") return [];
2139
2513
  throw error;
2140
2514
  }
2141
- if (!isJsonObject(value) || value.version !== 1 || !Array.isArray(value.machines)) throw new Error("Invalid device-host machine credential store");
2515
+ if (isJsonObject(value) && value.version === 1) return [];
2516
+ if (!isJsonObject(value) || value.version !== 2 || !Array.isArray(value.machines)) throw new Error("Invalid device-host machine credential store");
2142
2517
  const machines = [];
2143
2518
  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
- });
2519
+ const credential = parseMachineCredential(entry);
2520
+ if (!credential || machines.some((each) => each.machine === credential.machine)) throw new Error("Invalid device-host machine credential store");
2521
+ machines.push(credential);
2158
2522
  }
2159
2523
  return machines;
2160
2524
  }
@@ -2174,11 +2538,15 @@ function requireAutomaticMachine(role, machine) {
2174
2538
  remedy: `Enable it in remote.${role}PoolDisabled or select a machine explicitly.`
2175
2539
  });
2176
2540
  }
2177
- function validateAutomaticPool(role, config) {
2541
+ /**
2542
+ * Refuses a pool with no enabled member: local, or a configured remote approved for the role. `canHost` excludes
2543
+ * device-pool remotes that can never host, such as a Linux machine.
2544
+ */
2545
+ function validateAutomaticPool(role, config, canHost = () => true) {
2178
2546
  if (automaticMachineEnabled(role, "local", config)) return;
2179
2547
  const configured = config.remote?.machines;
2180
2548
  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;
2549
+ 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
2550
  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
2551
  }
2184
2552
  //#endregion
@@ -2192,7 +2560,7 @@ function debugLogDir() {
2192
2560
  return join(configDir(), "logs", "debug");
2193
2561
  }
2194
2562
  /** 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) {
2563
+ function debugLoggingEnabled(env = ambientEnv()) {
2196
2564
  const definition = settingDefinition("debug.logs");
2197
2565
  const raw = env[DEBUG_ENV];
2198
2566
  if (raw !== void 0 && raw !== "") {
@@ -2219,15 +2587,21 @@ function scrub(value, depth = 0) {
2219
2587
  * Debug-level records for one component (`cli` or `server`). Off by default; `STIM_DEBUG=1` or `debug.logs` turns it
2220
2588
  * on. Callers pass names, durations and codes, never arguments, environment values, tokens or tickets.
2221
2589
  */
2222
- function createDebugLog(component, { env = process.env, now = Date.now, base = () => ({}) } = {}) {
2590
+ function createDebugLog(component, { env, now = Date.now, base = () => ({}) } = {}) {
2223
2591
  let checkedAt = Number.NEGATIVE_INFINITY;
2224
2592
  let on = false;
2225
2593
  let unchecked = ROTATE_CHECK_BYTES;
2594
+ let checkedEnv;
2595
+ let checkedHome;
2226
2596
  const enabled = () => {
2227
2597
  const at = now();
2228
- if (at - checkedAt >= ENABLED_RECHECK_MS) {
2598
+ const activeEnv = env ?? ambientEnv();
2599
+ const home = configDir();
2600
+ if (activeEnv !== checkedEnv || home !== checkedHome || at - checkedAt >= ENABLED_RECHECK_MS) {
2229
2601
  checkedAt = at;
2230
- on = debugLoggingEnabled(env);
2602
+ checkedEnv = activeEnv;
2603
+ checkedHome = home;
2604
+ on = debugLoggingEnabled(activeEnv);
2231
2605
  }
2232
2606
  return on;
2233
2607
  };
@@ -2258,6 +2632,15 @@ function createDebugLog(component, { env = process.env, now = Date.now, base = (
2258
2632
  };
2259
2633
  }
2260
2634
  //#endregion
2635
+ //#region state/home-identity.ts
2636
+ /** The persistent, nonsecret identity of this Stim home; null until the CLI initializes it. */
2637
+ function readHomeIdentity(config = loadConfig()) {
2638
+ const id = config?.homeId;
2639
+ if (id === void 0) return null;
2640
+ if (!validHomeId(id)) throw Object.assign(/* @__PURE__ */ new Error(`Stim home identity is not a UUID. ${configCorruptRepair()}`), { code: "STIM_CONFIG_CORRUPT" });
2641
+ return id.toLowerCase();
2642
+ }
2643
+ //#endregion
2261
2644
  //#region state/ledgers.ts
2262
2645
  function ids(value) {
2263
2646
  return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry !== "") : [];
@@ -2692,7 +3075,8 @@ function expoContextFollower(onRecord, into) {
2692
3075
  return {
2693
3076
  push,
2694
3077
  settle,
2695
- flush
3078
+ flush,
3079
+ pending: () => pending !== null
2696
3080
  };
2697
3081
  }
2698
3082
  function includeBareErrorContext(all, matched, launchTs, sinceTs) {
@@ -2743,11 +3127,11 @@ function launchMarkersBySlot(records) {
2743
3127
  }
2744
3128
  return markers;
2745
3129
  }
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 [];
3130
+ /** Decides which records a query matches from the timeline's marker records alone. */
3131
+ function planLogQuery(markers, { slot, sources, minLevel, since, grep, errorsOnly, now } = {}) {
3132
+ const slotLaunched = slot !== void 0 && markers.some((record) => isLaunchMarker(record) && (record.slot ?? "default") === slot);
3133
+ const inTimeline = (record) => slot === void 0 || (record.slot ?? "default") === slot || slotLaunched && isSharedRecord(record);
3134
+ const all = markers.filter((record) => record.marker === true && inTimeline(record));
2751
3135
  const { launchTs, bundleTs, pageLoadTs } = errorsOnly ? markerWindow(all) : {
2752
3136
  launchTs: null,
2753
3137
  bundleTs: null,
@@ -2765,9 +3149,8 @@ function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, e
2765
3149
  bundleMarkerTs: bundleTs === null ? void 0 : bundleTs
2766
3150
  });
2767
3151
  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);
3152
+ const launches = newestByPlatform(all.filter((record) => record.src !== "metro" && !isWebPageRecord(record)), (record) => typeof record.platform === "string" ? record.platform : null);
3153
+ const bundles = newestByPlatform(all.filter((record) => record.src === "metro"), bundleMarkerPlatform);
2771
3154
  const slotPlatforms = slot === void 0 ? [] : [...launches.keys()].filter((platform) => platform !== null);
2772
3155
  const windowStart = (record) => {
2773
3156
  if (pageLoadTs !== null && isWebPageRecord(record)) return pageLoadTs - 1;
@@ -2783,13 +3166,58 @@ function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, e
2783
3166
  const platform = nativePlatform(record);
2784
3167
  return newestWhere(bundles, (marker) => marker === null || (platform ? marker === platform : slotPlatforms.length === 0 || slotPlatforms.includes(marker)));
2785
3168
  };
2786
- let matched = all.filter((record) => !otherPlatform(record) && recordMatches(record, {
2787
- ...criteria,
2788
- markerTs: windowStart(record),
2789
- bundleMarkerTs: bundleWindowStart(record)
2790
- }));
3169
+ const lowestWindowStart = (slots) => {
3170
+ if (!criteria.errorsOnly) return void 0;
3171
+ const starts = [];
3172
+ if (pageLoadTs !== null) starts.push(pageLoadTs - 1);
3173
+ const kinds = criteria.sources ?? [];
3174
+ if (criteria.includeAppDeviceErrors || kinds.some((src) => src !== "metro" && src !== "client")) {
3175
+ const possible = slot !== void 0 ? [slot] : slots ? [...slots] : null;
3176
+ if (!possible) return void 0;
3177
+ for (const value of possible) {
3178
+ const start = slotMarkers.get(value);
3179
+ if (start === void 0) return void 0;
3180
+ starts.push(start);
3181
+ }
3182
+ }
3183
+ if (kinds.includes("metro") || kinds.includes("client")) {
3184
+ if (criteria.markerTs === void 0) return void 0;
3185
+ starts.push(criteria.markerTs, ...launches.values());
3186
+ }
3187
+ return starts.length ? Math.min(...starts) : void 0;
3188
+ };
3189
+ return {
3190
+ inTimeline,
3191
+ matches: (record) => inTimeline(record) && !otherPlatform(record) && recordMatches(record, {
3192
+ ...criteria,
3193
+ markerTs: windowStart(record),
3194
+ bundleMarkerTs: bundleWindowStart(record)
3195
+ }),
3196
+ launchTs,
3197
+ sinceTs: criteria.sinceTs,
3198
+ excludesUpTo: (maxTs, slots) => {
3199
+ if (criteria.sinceTs !== void 0 && maxTs < criteria.sinceTs) return true;
3200
+ const start = lowestWindowStart(slots);
3201
+ return start !== void 0 && maxTs <= start;
3202
+ }
3203
+ };
3204
+ }
3205
+ function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, errorsOnly, errorContext, now } = {}) {
3206
+ const source = records ?? readLogRecords(dir);
3207
+ const plan = planLogQuery(source.filter((record) => record.marker === true), {
3208
+ slot,
3209
+ sources,
3210
+ minLevel,
3211
+ since,
3212
+ grep,
3213
+ errorsOnly,
3214
+ now
3215
+ });
3216
+ const all = source.filter(plan.inTimeline);
3217
+ if (all.length === 0) return [];
3218
+ let matched = all.filter(plan.matches);
2791
3219
  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;
3220
+ return errorContext ? includeBareErrorContext(all, attachExpoErrorContext(all, matched), plan.launchTs, plan.sinceTs) : matched;
2793
3221
  }
2794
3222
  /** Queries the JSON timeline, including Expo context fields on errors as `stim logs --json` does. */
2795
3223
  function queryJsonLogs(options = {}) {
@@ -2834,10 +3262,12 @@ function advanceTail(prev, chunk, size) {
2834
3262
  records
2835
3263
  };
2836
3264
  }
2837
- function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets = null, errorContext }) {
3265
+ function followLogs({ dir, onRecord, criteria = {}, intervalMs, offsets = null, errorContext }) {
2838
3266
  const follower = errorContext ? expoContextFollower(onRecord, errorContext) : null;
2839
3267
  const state = /* @__PURE__ */ new Map();
2840
3268
  const decoders = /* @__PURE__ */ new Map();
3269
+ let stopped = false;
3270
+ let contextTimer;
2841
3271
  for (const [name, size] of Object.entries(offsets || fileSizes(dir))) {
2842
3272
  state.set(name, {
2843
3273
  offset: size,
@@ -2847,6 +3277,7 @@ function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets =
2847
3277
  }
2848
3278
  function emit(records) {
2849
3279
  for (const record of records) {
3280
+ if (stopped) break;
2850
3281
  const matches = recordMatches(record, criteria);
2851
3282
  if (follower) follower.push(record, matches);
2852
3283
  else if (matches) onRecord(record);
@@ -2876,14 +3307,105 @@ function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets =
2876
3307
  emit(next.records);
2877
3308
  }
2878
3309
  function poll() {
2879
- for (const name of logFiles(dir)) try {
2880
- pollFile(name);
2881
- } catch {}
3310
+ if (stopped) return;
3311
+ for (const name of logFiles(dir)) {
3312
+ if (stopped) break;
3313
+ try {
3314
+ pollFile(name);
3315
+ } catch {}
3316
+ }
3317
+ if (intervalMs !== void 0) follower?.settle();
3318
+ else if (!contextTimer && follower?.pending()) {
3319
+ follower.settle();
3320
+ contextTimer = setTimeout(settleContext, 500);
3321
+ }
3322
+ }
3323
+ function settleContext() {
3324
+ if (stopped) return;
3325
+ poll();
3326
+ contextTimer = void 0;
3327
+ if (stopped) return;
2882
3328
  follower?.settle();
3329
+ if (follower?.pending()) contextTimer = setTimeout(settleContext, 500);
3330
+ }
3331
+ let watcher;
3332
+ let directory;
3333
+ let scheduled;
3334
+ let repairTimer;
3335
+ function closeWatch() {
3336
+ const previous = watcher;
3337
+ watcher = void 0;
3338
+ directory = void 0;
3339
+ previous?.close();
3340
+ }
3341
+ function schedulePoll() {
3342
+ if (stopped || scheduled) return;
3343
+ scheduled = setImmediate(() => {
3344
+ scheduled = void 0;
3345
+ poll();
3346
+ });
3347
+ }
3348
+ function sameDirectory() {
3349
+ if (!watcher || !directory) return false;
3350
+ try {
3351
+ const current = statSync(dir);
3352
+ return current.dev === directory.dev && current.ino === directory.ino;
3353
+ } catch {
3354
+ return false;
3355
+ }
3356
+ }
3357
+ function attachWatch() {
3358
+ if (stopped || watcher) return false;
3359
+ try {
3360
+ const current = statSync(dir);
3361
+ const next = watch(dir, { persistent: false }, (event) => {
3362
+ if (stopped) return;
3363
+ if (event === "rename" && !sameDirectory()) {
3364
+ closeWatch();
3365
+ armRepair(attachWatch() ? 500 : void 0);
3366
+ }
3367
+ schedulePoll();
3368
+ });
3369
+ watcher = next;
3370
+ directory = current;
3371
+ const lost = () => {
3372
+ if (watcher !== next) return;
3373
+ closeWatch();
3374
+ armRepair();
3375
+ };
3376
+ next.once("error", lost);
3377
+ next.once("close", lost);
3378
+ return true;
3379
+ } catch {}
3380
+ return false;
3381
+ }
3382
+ function armRepair(delay = watcher ? 1e4 : 500) {
3383
+ clearTimeout(repairTimer);
3384
+ if (stopped) return;
3385
+ repairTimer = setTimeout(() => {
3386
+ let attached = false;
3387
+ if (!sameDirectory()) {
3388
+ closeWatch();
3389
+ attached = attachWatch();
3390
+ }
3391
+ poll();
3392
+ armRepair(attached ? 500 : void 0);
3393
+ }, delay);
3394
+ }
3395
+ const timer = intervalMs === void 0 ? void 0 : setInterval(poll, intervalMs);
3396
+ if (intervalMs === void 0) {
3397
+ attachWatch();
3398
+ poll();
3399
+ armRepair(500);
2883
3400
  }
2884
- const timer = setInterval(poll, intervalMs);
2885
3401
  return function stop() {
3402
+ if (stopped) return;
3403
+ stopped = true;
2886
3404
  clearInterval(timer);
3405
+ clearTimeout(repairTimer);
3406
+ clearTimeout(contextTimer);
3407
+ clearImmediate(scheduled);
3408
+ closeWatch();
2887
3409
  follower?.flush();
2888
3410
  };
2889
3411
  }
@@ -2912,6 +3434,378 @@ function sortByTs(records) {
2912
3434
  });
2913
3435
  }
2914
3436
  //#endregion
3437
+ //#region state/log-reader.ts
3438
+ const BLOCK_BYTES = 65536;
3439
+ const MAX_CACHED_GENERATIONS = 512;
3440
+ const NEWLINE = 10;
3441
+ const VALIDATE_BYTES = 256;
3442
+ const cache = /* @__PURE__ */ new Map();
3443
+ const indexing = /* @__PURE__ */ new Map();
3444
+ function tsKey(record) {
3445
+ return typeof record.ts === "number" && Number.isFinite(record.ts) ? record.ts : Infinity;
3446
+ }
3447
+ function compare(a, b) {
3448
+ return a.ts - b.ts || a.rank - b.rank || a.generation - b.generation || a.position - b.position;
3449
+ }
3450
+ function markerTs(record) {
3451
+ const ts = tsKey(record);
3452
+ return ts === Infinity ? -Infinity : ts;
3453
+ }
3454
+ function keepNewer(markers, key, marker) {
3455
+ const known = markers.get(key);
3456
+ if (!known || markerTs(marker) > markerTs(known)) markers.set(key, marker);
3457
+ }
3458
+ function markerKey(record) {
3459
+ const platform = record.src === "metro" ? bundleMarkerPlatform(record) : typeof record.platform === "string" ? record.platform : null;
3460
+ const ts = tsKey(record);
3461
+ return {
3462
+ key: JSON.stringify([
3463
+ record.src ?? null,
3464
+ record.slot === void 0 ? 0 : [record.slot],
3465
+ platform
3466
+ ]),
3467
+ marker: {
3468
+ src: record.src,
3469
+ marker: true,
3470
+ ...ts === Infinity ? {} : { ts },
3471
+ ...record.slot === void 0 ? {} : { slot: record.slot },
3472
+ ...platform === null ? {} : { platform }
3473
+ }
3474
+ };
3475
+ }
3476
+ function emptyIndex() {
3477
+ return {
3478
+ size: 0,
3479
+ head: Buffer.alloc(0),
3480
+ tail: Buffer.alloc(0),
3481
+ blocks: [],
3482
+ minTs: null,
3483
+ markers: /* @__PURE__ */ new Map(),
3484
+ launchAttempts: [],
3485
+ slots: /* @__PURE__ */ new Set()
3486
+ };
3487
+ }
3488
+ function indexBlock(index, buffer, start) {
3489
+ let maxTs = -Infinity;
3490
+ let minTs = Infinity;
3491
+ const sources = /* @__PURE__ */ new Set();
3492
+ const levels = /* @__PURE__ */ new Set();
3493
+ for (let lineStart = 0; lineStart < buffer.length;) {
3494
+ const lineEnd = buffer.indexOf(NEWLINE, lineStart);
3495
+ const record = parseNdjsonLine(buffer.toString("utf8", lineStart, lineEnd));
3496
+ if (record) {
3497
+ const value = tsKey(record);
3498
+ maxTs = Math.max(maxTs, value);
3499
+ minTs = Math.min(minTs, value);
3500
+ if (value !== Infinity && (index.minTs === null || value < index.minTs)) index.minTs = value;
3501
+ sources.add(record.src);
3502
+ levels.add(record.level);
3503
+ index.slots.add(record.slot ?? "default");
3504
+ if (record.marker === true) {
3505
+ const { key, marker } = markerKey(record);
3506
+ keepNewer(index.markers, key, marker);
3507
+ }
3508
+ if (record.event === "launch_attempt") index.launchAttempts.push({
3509
+ record,
3510
+ position: start + lineStart
3511
+ });
3512
+ }
3513
+ lineStart = lineEnd + 1;
3514
+ }
3515
+ return {
3516
+ start,
3517
+ end: start + buffer.length,
3518
+ maxTs,
3519
+ minTs,
3520
+ sources,
3521
+ levels
3522
+ };
3523
+ }
3524
+ async function read(handle, position, length) {
3525
+ const buffer = Buffer.allocUnsafe(length);
3526
+ let filled = 0;
3527
+ while (filled < length) {
3528
+ const { bytesRead } = await handle.read(buffer, filled, length - filled, position + filled);
3529
+ if (bytesRead === 0) break;
3530
+ filled += bytesRead;
3531
+ }
3532
+ return buffer.subarray(0, filled);
3533
+ }
3534
+ async function stillExtends(handle, index, size) {
3535
+ if (size < index.size) return false;
3536
+ const head = await read(handle, 0, index.head.length);
3537
+ const tail = await read(handle, index.size - index.tail.length, index.tail.length);
3538
+ return head.equals(index.head) && tail.equals(index.tail);
3539
+ }
3540
+ function resumeFrom(index) {
3541
+ const last = index.blocks.at(-1);
3542
+ if (!last || last.end - last.start >= BLOCK_BYTES) return {
3543
+ ...index,
3544
+ blocks: [...index.blocks]
3545
+ };
3546
+ return {
3547
+ ...index,
3548
+ size: last.start,
3549
+ blocks: index.blocks.slice(0, -1),
3550
+ launchAttempts: index.launchAttempts.filter((attempt) => attempt.position < last.start)
3551
+ };
3552
+ }
3553
+ async function indexGeneration(handle, identity, size, signal) {
3554
+ signal?.throwIfAborted();
3555
+ const pending = indexing.get(identity);
3556
+ if (pending) {
3557
+ await new Promise((resolve, reject) => {
3558
+ const cancel = () => reject(signal?.reason);
3559
+ signal?.addEventListener("abort", cancel, { once: true });
3560
+ if (signal?.aborted) cancel();
3561
+ pending.then(() => resolve(), () => resolve()).finally(() => signal?.removeEventListener("abort", cancel));
3562
+ });
3563
+ return indexGeneration(handle, identity, size, signal);
3564
+ }
3565
+ const building = buildGenerationIndex(handle, identity, size, signal);
3566
+ indexing.set(identity, building);
3567
+ try {
3568
+ return await building;
3569
+ } finally {
3570
+ indexing.delete(identity);
3571
+ }
3572
+ }
3573
+ async function buildGenerationIndex(handle, identity, size, signal) {
3574
+ signal?.throwIfAborted();
3575
+ const cached = cache.get(identity);
3576
+ const known = cached && await stillExtends(handle, cached, size) ? cached : null;
3577
+ if (known && known.size === size) {
3578
+ cache.delete(identity);
3579
+ cache.set(identity, known);
3580
+ return known;
3581
+ }
3582
+ const index = known ? resumeFrom(known) : emptyIndex();
3583
+ const next = {
3584
+ ...index,
3585
+ markers: new Map(index.markers),
3586
+ launchAttempts: [...index.launchAttempts],
3587
+ slots: new Set(index.slots)
3588
+ };
3589
+ let offset = next.size;
3590
+ let length = BLOCK_BYTES;
3591
+ while (offset < size) {
3592
+ signal?.throwIfAborted();
3593
+ const buffer = await read(handle, offset, Math.min(length, size - offset));
3594
+ const end = buffer.lastIndexOf(NEWLINE);
3595
+ if (end === -1) {
3596
+ if (buffer.length < Math.min(length, size - offset) || offset + buffer.length >= size) break;
3597
+ length *= 2;
3598
+ continue;
3599
+ }
3600
+ next.blocks.push(indexBlock(next, buffer.subarray(0, end + 1), offset));
3601
+ offset += end + 1;
3602
+ length = BLOCK_BYTES;
3603
+ signal?.throwIfAborted();
3604
+ await setImmediate$1();
3605
+ }
3606
+ next.size = offset;
3607
+ next.head = await read(handle, 0, Math.min(VALIDATE_BYTES, offset));
3608
+ next.tail = await read(handle, Math.max(0, offset - VALIDATE_BYTES), Math.min(VALIDATE_BYTES, offset));
3609
+ cache.delete(identity);
3610
+ cache.set(identity, next);
3611
+ while (cache.size > MAX_CACHED_GENERATIONS) cache.delete(cache.keys().next().value);
3612
+ return next;
3613
+ }
3614
+ /**
3615
+ * Opens `dir` for reading. With `order: 'timeline'`, native crash files follow every other file at equal
3616
+ * timestamps, as `readLogTimeline` orders them; with `'name'`, every file is ranked by name, as `readLogRecords`.
3617
+ */
3618
+ async function openLogDir(dir, { order = "timeline", signal } = {}) {
3619
+ signal?.throwIfAborted();
3620
+ let names;
3621
+ try {
3622
+ names = (await readdir(dir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".ndjson")).map((entry) => entry.name).toSorted();
3623
+ } catch {
3624
+ names = [];
3625
+ }
3626
+ const ranked = order === "name" ? names : [...names.filter((name) => !name.startsWith(NATIVE_CRASH_LOG_PREFIX)), ...names.filter((name) => name.startsWith(NATIVE_CRASH_LOG_PREFIX))];
3627
+ const generations = [];
3628
+ const sizes = {};
3629
+ const close = async () => {
3630
+ await Promise.all(generations.map((generation) => generation.handle.close().catch(() => {})));
3631
+ };
3632
+ try {
3633
+ for (const [rank, name] of ranked.entries()) for (const [generation, path] of [rotatedLogPath(join(dir, name)), join(dir, name)].entries()) {
3634
+ let handle;
3635
+ try {
3636
+ handle = await open(path, "r");
3637
+ } catch {
3638
+ continue;
3639
+ }
3640
+ try {
3641
+ const stat = await handle.stat({ bigint: true });
3642
+ const index = await indexGeneration(handle, `${stat.dev}:${stat.ino}`, Number(stat.size), signal);
3643
+ generations.push({
3644
+ handle,
3645
+ rank,
3646
+ generation,
3647
+ crash: name.startsWith(NATIVE_CRASH_LOG_PREFIX),
3648
+ index
3649
+ });
3650
+ if (generation === 1) sizes[name] = index.size;
3651
+ } catch (error) {
3652
+ await handle.close().catch(() => {});
3653
+ throw error;
3654
+ }
3655
+ }
3656
+ } catch (error) {
3657
+ await close();
3658
+ throw error;
3659
+ }
3660
+ let firstTs;
3661
+ for (const { index } of generations) if (index.minTs !== null && (firstTs === void 0 || index.minTs < firstTs)) firstTs = index.minTs;
3662
+ return {
3663
+ sizes,
3664
+ firstTs,
3665
+ launchAttempts: () => generations.filter((generation) => !generation.crash).flatMap(({ index, rank, generation }) => index.launchAttempts.map(({ record, position }) => ({
3666
+ ts: tsKey(record),
3667
+ rank,
3668
+ generation,
3669
+ position,
3670
+ record
3671
+ }))).toSorted(compare).map((entry) => entry.record),
3672
+ query: (query = {}) => queryGenerations(generations, query, signal),
3673
+ batches: (query = {}) => queryBatches(generations, query, signal),
3674
+ filter: (query = {}) => planLogQuery(generationMarkers(generations), query).matches,
3675
+ close
3676
+ };
3677
+ }
3678
+ function blockMatches(block, { errorsOnly, minLevel, sources }) {
3679
+ if (errorsOnly && !block.levels.has("error") && !block.levels.has("fatal")) return false;
3680
+ if (minLevel && ![...block.levels].some((level) => levelRank(level) >= levelRank(minLevel))) return false;
3681
+ return !sources?.length || sources.some((source) => block.sources.has(source)) || Boolean(errorsOnly && block.sources.has("device"));
3682
+ }
3683
+ async function select(generations, extra, { matches, excludesUpTo, mayContain, tail, first = false }, signal) {
3684
+ if (tail !== void 0 && tail <= 0) return [];
3685
+ const heap = new BoundedSelection(compare, tail, first);
3686
+ const extraRank = generations.length ? Math.max(...generations.map((generation) => generation.rank)) + 1 : 0;
3687
+ for (const [position, record] of extra.entries()) {
3688
+ const entry = {
3689
+ ts: tsKey(record),
3690
+ rank: extraRank,
3691
+ generation: 0,
3692
+ position,
3693
+ record
3694
+ };
3695
+ if (matches(record, entry)) heap.keep(entry);
3696
+ }
3697
+ const blocks = generations.flatMap((generation) => generation.index.blocks.map((block) => ({
3698
+ generation,
3699
+ block
3700
+ }))).toSorted((a, b) => (first ? a.block.minTs - b.block.minTs : b.block.maxTs - a.block.maxTs) || 0);
3701
+ for (const { generation, block } of blocks) {
3702
+ if (excludesUpTo(block.maxTs)) {
3703
+ if (first) continue;
3704
+ break;
3705
+ }
3706
+ if (first && tail !== void 0 && heap.size >= tail && block.minTs > heap.peek().ts) break;
3707
+ if (!first && tail !== void 0 && heap.size >= tail && block.maxTs < heap.peek().ts) break;
3708
+ signal?.throwIfAborted();
3709
+ if (!mayContain(block, generation)) continue;
3710
+ const buffer = await read(generation.handle, block.start, block.end - block.start);
3711
+ for (let lineStart = 0; lineStart < buffer.length;) {
3712
+ const lineEnd = buffer.indexOf(NEWLINE, lineStart);
3713
+ if (lineEnd === -1) break;
3714
+ const record = parseNdjsonLine(buffer.toString("utf8", lineStart, lineEnd));
3715
+ const position = block.start + lineStart;
3716
+ lineStart = lineEnd + 1;
3717
+ if (!record) continue;
3718
+ const entry = {
3719
+ ts: tsKey(record),
3720
+ rank: generation.rank,
3721
+ generation: generation.generation,
3722
+ position,
3723
+ record
3724
+ };
3725
+ if (matches(record, entry)) heap.keep(entry);
3726
+ }
3727
+ await setImmediate$1();
3728
+ }
3729
+ return heap.sorted();
3730
+ }
3731
+ function generationMarkers(generations) {
3732
+ const markers = /* @__PURE__ */ new Map();
3733
+ for (const { index } of generations) for (const [key, marker] of index.markers) keepNewer(markers, key, marker);
3734
+ return [...markers.values()];
3735
+ }
3736
+ async function selectQuery(generations, { tail, extra = [], ...options }, signal, page) {
3737
+ signal?.throwIfAborted();
3738
+ const slots = /* @__PURE__ */ new Set(["default"]);
3739
+ for (const { index } of generations) for (const slot of index.slots) slots.add(slot);
3740
+ for (const record of extra) slots.add(record.slot ?? "default");
3741
+ const plan = planLogQuery([...generationMarkers(generations), ...extra.filter((record) => record.marker === true)], options);
3742
+ return select(generations, extra, {
3743
+ matches: (record, entry) => plan.matches(record) && (!page?.after || compare(entry, page.after) > 0),
3744
+ excludesUpTo: (maxTs) => plan.excludesUpTo(maxTs, slots),
3745
+ mayContain: (block, generation) => blockMatches(block, options) && (!page?.after || compare({
3746
+ ts: block.maxTs,
3747
+ rank: generation.rank,
3748
+ generation: generation.generation,
3749
+ position: block.end
3750
+ }, page.after) > 0),
3751
+ tail: page?.size ?? tail,
3752
+ first: page !== void 0
3753
+ }, signal);
3754
+ }
3755
+ async function queryGenerations(generations, query, signal) {
3756
+ return withErrorContext(generations, query, await selectQuery(generations, query, signal), signal);
3757
+ }
3758
+ async function* queryBatches(generations, query, signal) {
3759
+ const fixed = {
3760
+ ...query,
3761
+ now: query.now ?? Date.now()
3762
+ };
3763
+ let after;
3764
+ for (;;) {
3765
+ const selected = await selectQuery(generations, fixed, signal, {
3766
+ after,
3767
+ size: 500
3768
+ });
3769
+ if (!selected.length) return;
3770
+ yield await withErrorContext(generations, fixed, selected, signal);
3771
+ after = selected.at(-1);
3772
+ }
3773
+ }
3774
+ async function withErrorContext(generations, { extra = [], ...options }, selected, signal) {
3775
+ const matched = selected.map((entry) => entry.record);
3776
+ if (!options.errorsOnly) return matched;
3777
+ const errors = matched.filter((record) => record.src === "metro" && record.raw === true && (record.level === "error" || record.level === "fatal") && typeof record.msg === "string");
3778
+ if (!errors.length) return matched;
3779
+ const contextual = /* @__PURE__ */ new Map();
3780
+ for (const error of errors) {
3781
+ const [first] = await select(generations, extra, {
3782
+ matches: (record) => record.ts === error.ts && record.src === error.src && record.msg === error.msg && record.event === error.event,
3783
+ excludesUpTo: (maxTs) => maxTs < tsKey(error),
3784
+ mayContain: (block) => block.sources.has("metro"),
3785
+ tail: 1,
3786
+ first: true
3787
+ }, signal);
3788
+ if (!first) continue;
3789
+ let limit = 32;
3790
+ for (;;) {
3791
+ const stream = (await select(generations, extra, {
3792
+ 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),
3793
+ excludesUpTo: (maxTs) => maxTs < tsKey(error),
3794
+ mayContain: (block) => block.sources.has("metro"),
3795
+ tail: limit,
3796
+ first: true
3797
+ }, signal)).map((entry) => entry.record);
3798
+ const index = stream.findIndex((record) => record.ts === error.ts && record.msg === error.msg);
3799
+ if (index < 0 || stream.slice(index + 1).some((record) => record.raw === true && !isExpoErrorContext(record, error.event)) || stream.length < limit) {
3800
+ contextual.set(error, attachExpoErrorContext(stream, [error], "field")[0]);
3801
+ break;
3802
+ }
3803
+ limit *= 2;
3804
+ }
3805
+ }
3806
+ return matched.map((record) => contextual.get(record) ?? record);
3807
+ }
3808
+ //#endregion
2915
3809
  //#region state/recordings.ts
2916
3810
  /** The footage stim-server keeps per device: the last 15 minutes recorded, however long ago. */
2917
3811
  const RECORDING_FOOTAGE_MS = 9e5;
@@ -2944,44 +3838,105 @@ function listDir(dir) {
2944
3838
  return [];
2945
3839
  }
2946
3840
  }
3841
+ async function listDirAsync(dir, signal) {
3842
+ signal?.throwIfAborted();
3843
+ try {
3844
+ const names = await readdir(dir);
3845
+ signal?.throwIfAborted();
3846
+ return names;
3847
+ } catch {
3848
+ signal?.throwIfAborted();
3849
+ return [];
3850
+ }
3851
+ }
3852
+ function segmentName(name, closedOnly) {
3853
+ const closed = SEGMENT.exec(name);
3854
+ const part = closed ? null : PART.exec(name);
3855
+ if (!closed && (closedOnly || !part)) return null;
3856
+ const start = Number((closed ?? part)[1]);
3857
+ return {
3858
+ start,
3859
+ end: closed ? Math.max(start, Number(closed[2])) : null
3860
+ };
3861
+ }
3862
+ function segmentRecord(file, range, metadata) {
3863
+ return {
3864
+ file,
3865
+ start: range.start,
3866
+ end: range.end ?? Math.max(range.start, Math.floor(metadata.mtimeMs)),
3867
+ bytes: metadata.size,
3868
+ open: range.end === null
3869
+ };
3870
+ }
3871
+ function deviceName(name) {
3872
+ const match = DEVICE.exec(name);
3873
+ return match ? {
3874
+ platform: match[1],
3875
+ slot: match[2]
3876
+ } : null;
3877
+ }
2947
3878
  /** The segments in one device directory, oldest first. Files that are not segments are left out; `closedOnly` excludes files still being written. */
2948
3879
  function listSegments(dir, closedOnly = false) {
2949
3880
  const segments = [];
2950
3881
  for (const name of listDir(dir)) {
2951
- const closed = SEGMENT.exec(name);
2952
- const part = closed ? null : PART.exec(name);
2953
- if (!closed && (closedOnly || !part)) continue;
2954
- let stat;
3882
+ const range = segmentName(name, closedOnly);
3883
+ if (!range) continue;
3884
+ const file = join(dir, name);
3885
+ try {
3886
+ segments.push(segmentRecord(file, range, statSync(file)));
3887
+ } catch {}
3888
+ }
3889
+ return segments.toSorted((a, b) => a.start - b.start);
3890
+ }
3891
+ /** Reads current segment files asynchronously, oldest first; cancellation propagates. */
3892
+ async function listSegmentsAsync(dir, closedOnly = false, signal) {
3893
+ const segments = [];
3894
+ for (const name of await listDirAsync(dir, signal)) {
3895
+ signal?.throwIfAborted();
3896
+ const range = segmentName(name, closedOnly);
3897
+ if (!range) continue;
3898
+ const file = join(dir, name);
2955
3899
  try {
2956
- stat = statSync(join(dir, name));
3900
+ const metadata = await stat(file);
3901
+ signal?.throwIfAborted();
3902
+ segments.push(segmentRecord(file, range, metadata));
2957
3903
  } catch {
2958
- continue;
3904
+ signal?.throwIfAborted();
2959
3905
  }
2960
- const start = Number((closed ?? part)[1]);
2961
- segments.push({
2962
- file: join(dir, name),
2963
- start,
2964
- end: closed ? Math.max(start, Number(closed[2])) : Math.max(start, Math.floor(stat.mtimeMs)),
2965
- bytes: stat.size,
2966
- open: part !== null
2967
- });
2968
3906
  }
3907
+ signal?.throwIfAborted();
2969
3908
  return segments.toSorted((a, b) => a.start - b.start);
2970
3909
  }
2971
3910
  /** Every device slot recorded under a workspace's recordings directory. */
2972
3911
  function listRecordedDevices(recordingsDir, closedOnly = false) {
2973
3912
  return listDir(recordingsDir).flatMap((name) => {
2974
- const match = DEVICE.exec(name);
2975
- if (!match) return [];
3913
+ const device = deviceName(name);
3914
+ if (!device) return [];
2976
3915
  const dir = join(recordingsDir, name);
2977
3916
  return [{
2978
- platform: match[1],
2979
- slot: match[2],
3917
+ ...device,
2980
3918
  dir,
2981
3919
  segments: listSegments(dir, closedOnly)
2982
3920
  }];
2983
3921
  });
2984
3922
  }
3923
+ /** Reads current device slots and their segment files asynchronously; cancellation propagates. */
3924
+ async function listRecordedDevicesAsync(recordingsDir, closedOnly = false, signal) {
3925
+ const devices = [];
3926
+ for (const name of await listDirAsync(recordingsDir, signal)) {
3927
+ signal?.throwIfAborted();
3928
+ const device = deviceName(name);
3929
+ if (!device) continue;
3930
+ const dir = join(recordingsDir, name);
3931
+ devices.push({
3932
+ ...device,
3933
+ dir,
3934
+ segments: await listSegmentsAsync(dir, closedOnly, signal)
3935
+ });
3936
+ }
3937
+ signal?.throwIfAborted();
3938
+ return devices;
3939
+ }
2985
3940
  function recordedBytes(devices) {
2986
3941
  return devices.reduce((sum, device) => sum + device.segments.reduce((total, s) => total + s.bytes, 0), 0);
2987
3942
  }
@@ -3042,10 +3997,13 @@ function validRunId(value) {
3042
3997
  }
3043
3998
  let current = null;
3044
3999
  /**
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.
4000
+ * The run id of the ambient operation context, else of this CLI invocation: `STIM_RUN_ID` when it holds a valid id
4001
+ * (Stim Desktop passes one), else a random one. The CLI entry exports it back to `STIM_RUN_ID` so processes the run
4002
+ * starts inherit it; this function does not.
3047
4003
  */
3048
4004
  function runId(env = process.env) {
4005
+ const context = ambientContext();
4006
+ if (context) return context.runId;
3049
4007
  if (current === null) current = validRunId(env["STIM_RUN_ID"]) ?? randomBytes(6).toString("hex");
3050
4008
  return current;
3051
4009
  }
@@ -3348,12 +4306,16 @@ function readPullRequestCache(path) {
3348
4306
  if (typeof entry.checkedAt !== "string") return null;
3349
4307
  const pullRequest = pullRequestOf(entry.pullRequest);
3350
4308
  if (pullRequest === void 0) return null;
4309
+ const { containsHead, endedAt } = entry;
3351
4310
  return {
3352
4311
  path,
3353
4312
  branch: entry.branch,
3354
4313
  head: entry.head,
3355
4314
  checkedAt: entry.checkedAt,
3356
- pullRequest
4315
+ pullRequest,
4316
+ ...typeof containsHead === "boolean" ? { containsHead } : {},
4317
+ ...endedAt === null || typeof endedAt === "number" && Number.isFinite(endedAt) ? { endedAt } : {},
4318
+ ...entry.partial === true ? { partial: true } : {}
3357
4319
  };
3358
4320
  }
3359
4321
  /** The cached size of the folder at `path`, or null before it was measured. */
@@ -3453,7 +4415,7 @@ function readAgentSessionsCache() {
3453
4415
  //#region state/stim-build.ts
3454
4416
  /**
3455
4417
  * 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.
4418
+ * bytes of a `stim` `dist` directory. A client and a remote machine offload only when both compute the same one.
3457
4419
  */
3458
4420
  function stimBuildDigest(dir) {
3459
4421
  try {
@@ -3718,7 +4680,22 @@ function lastUseFrom(state, logMtimes) {
3718
4680
  return candidates.length ? Math.max(...candidates) : NaN;
3719
4681
  }
3720
4682
  const MAINTENANCE_LOG_NAME = "maintenance.ndjson";
4683
+ /**
4684
+ * When the workspace was last used, in epoch milliseconds. A workspace that recorded no use falls back to the mtime
4685
+ * of its workspace.json, which is written once when the workspace is created, so it is a lower bound on the last
4686
+ * use. NaN when its state.json exists but does not parse, or when workspace.json cannot be read either.
4687
+ */
3721
4688
  function workspaceLastUsed(root) {
4689
+ const used = recordedLastUse(root);
4690
+ if (Number.isFinite(used)) return used;
4691
+ if (existsSync(workspaceStateFile(root)) && readWorkspaceState(root) === null) return NaN;
4692
+ try {
4693
+ return statSync(workspaceMetadataFile(root)).mtimeMs;
4694
+ } catch {
4695
+ return NaN;
4696
+ }
4697
+ }
4698
+ function recordedLastUse(root) {
3722
4699
  const logs = workspaceLogsDir(root);
3723
4700
  let mtimes = [];
3724
4701
  try {
@@ -3796,7 +4773,7 @@ function readViewedDevices(dir = deviceViewersDir()) {
3796
4773
  //#endregion
3797
4774
  //#region state/stats.ts
3798
4775
  const STATS_VERSION = 1;
3799
- /** Where a compiling build ran: here, on a remote Mac, or here after offloading it failed. */
4776
+ /** Where a compiling build ran: here, on a remote machine, or here after offloading it failed. */
3800
4777
  const PLACEMENT_DECISIONS = [
3801
4778
  "here",
3802
4779
  "offloaded",
@@ -3944,7 +4921,7 @@ function normalize(record) {
3944
4921
  const source = record.projects;
3945
4922
  if (source && typeof source === "object" && !Array.isArray(source)) for (const [key, scope] of Object.entries(source)) projects[key] = normalizeScope(scope);
3946
4923
  const history = {};
3947
- if (isObject(record.history)) for (const [key, scope] of Object.entries(record.history)) {
4924
+ if (isJsonObject(record.history)) for (const [key, scope] of Object.entries(record.history)) {
3948
4925
  const normalized = normalizeHistory(scope);
3949
4926
  if (Object.keys(normalized).length) history[key] = normalized;
3950
4927
  }
@@ -3952,8 +4929,8 @@ function normalize(record) {
3952
4929
  const capacityRefusals = Array.isArray(record.capacityRefusals) ? record.capacityRefusals.flatMap(normalizeCapacityRefusal) : [];
3953
4930
  const capacityWaits = Array.isArray(record.capacityWaits) ? record.capacityWaits.flatMap(normalizeCapacityWait) : [];
3954
4931
  const buildMachines = {};
3955
- if (isObject(record.buildMachines)) {
3956
- for (const [name, totals] of Object.entries(record.buildMachines)) if (isObject(totals)) buildMachines[name] = normalizeMachineTotals(totals);
4932
+ if (isJsonObject(record.buildMachines)) {
4933
+ for (const [name, totals] of Object.entries(record.buildMachines)) if (isJsonObject(totals)) buildMachines[name] = normalizeMachineTotals(totals);
3957
4934
  }
3958
4935
  return {
3959
4936
  version: 1,
@@ -3967,7 +4944,7 @@ function normalize(record) {
3967
4944
  };
3968
4945
  }
3969
4946
  function normalizePlacement(value) {
3970
- if (!isObject(value)) return [];
4947
+ if (!isJsonObject(value)) return [];
3971
4948
  const { decision, platform, reason, project, machine } = value;
3972
4949
  if (!PLACEMENT_DECISIONS.includes(decision) || !PLATFORMS.includes(platform)) return [];
3973
4950
  if (typeof reason !== "string" || typeof project !== "string") return [];
@@ -3990,7 +4967,7 @@ function normalizePlacement(value) {
3990
4967
  }];
3991
4968
  }
3992
4969
  function normalizeCapacityRefusal(value) {
3993
- if (!isObject(value)) return [];
4970
+ if (!isJsonObject(value)) return [];
3994
4971
  const { at, kind, platform, max, workspace } = value;
3995
4972
  if (kind !== "device" || !PLATFORMS.includes(platform)) return [];
3996
4973
  if (typeof at !== "string" || !Number.isFinite(Date.parse(at))) return [];
@@ -4005,7 +4982,7 @@ function normalizeCapacityRefusal(value) {
4005
4982
  }];
4006
4983
  }
4007
4984
  function normalizeCapacityWait(value) {
4008
- if (!isObject(value) || value.kind !== "device-wait") return [];
4985
+ if (!isJsonObject(value) || value.kind !== "device-wait") return [];
4009
4986
  const refusal = normalizeCapacityRefusal({
4010
4987
  ...value,
4011
4988
  kind: "device"
@@ -4029,20 +5006,17 @@ function normalizeMachineTotals(totals) {
4029
5006
  ...typeof totals.lastFallbackAt === "string" ? { lastFallbackAt: totals.lastFallbackAt } : {}
4030
5007
  };
4031
5008
  }
4032
- function isObject(value) {
4033
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4034
- }
4035
5009
  function normalizeHistory(scope) {
4036
5010
  const history = {};
4037
- if (!isObject(scope)) return history;
5011
+ if (!isJsonObject(scope)) return history;
4038
5012
  for (const platform of PLATFORMS) {
4039
5013
  const lists = scope[platform];
4040
- if (!isObject(lists)) continue;
5014
+ if (!isJsonObject(lists)) continue;
4041
5015
  const normalized = {};
4042
5016
  for (const outcome of OUTCOMES) {
4043
5017
  const samples = lists[outcome];
4044
5018
  if (!Array.isArray(samples)) continue;
4045
- const kept = samples.filter(isObject).flatMap((sample) => {
5019
+ const kept = samples.filter(isJsonObject).flatMap((sample) => {
4046
5020
  const durationMs = wholeMs(sample.durationMs);
4047
5021
  if (durationMs <= 0) return [];
4048
5022
  return [{
@@ -4060,7 +5034,7 @@ function normalizeHistory(scope) {
4060
5034
  }
4061
5035
  function wholePhases(phases) {
4062
5036
  const out = {};
4063
- if (!isObject(phases)) return out;
5037
+ if (!isJsonObject(phases)) return out;
4064
5038
  for (const [name, ms] of Object.entries(phases)) {
4065
5039
  const value = Number(ms);
4066
5040
  if (Number.isFinite(value) && value >= 0) out[name] = Math.round(value);
@@ -4187,6 +5161,8 @@ const DEVICE_HOST_LIMITS = {
4187
5161
  logsMs: 15e3,
4188
5162
  killGraceMs: 5e3
4189
5163
  };
5164
+ /** How many parked sessions one reserve inspects, and possibly retires, before it creates a device. */
5165
+ const HOSTED_PARKED_CANDIDATES = 3;
4190
5166
  /** How long a cancelled hosted worker run can take to settle: SIGTERM, SIGKILL after the grace, then it is abandoned. */
4191
5167
  function hostedWorkerSettleMs(limits = DEVICE_HOST_LIMITS) {
4192
5168
  return limits.killGraceMs * 2;
@@ -4570,6 +5546,10 @@ function parseMacosRecord(value) {
4570
5546
  startedAt: b.startedAt,
4571
5547
  ...typeof b.finishedAt === "string" ? { finishedAt: b.finishedAt } : {},
4572
5548
  ...typeof b.durationMs === "number" ? { durationMs: b.durationMs } : {},
5549
+ ...typeof b.fingerprint === "string" ? { fingerprint: b.fingerprint } : {},
5550
+ ...typeof b.cacheKey === "string" ? { cacheKey: b.cacheKey } : {},
5551
+ ...typeof b.cacheHit === "boolean" ? { cacheHit: b.cacheHit } : {},
5552
+ ...b.cacheSource === "local" || b.cacheSource === "provider" || b.cacheSource === "peer" ? { cacheSource: b.cacheSource } : {},
4573
5553
  ...typeof b.error === "string" ? { error: b.error } : {},
4574
5554
  ...typeof b.buildMachine === "string" ? { buildMachine: b.buildMachine } : {},
4575
5555
  ...typeof b.builtOn === "string" ? { builtOn: b.builtOn } : {},
@@ -4845,7 +5825,14 @@ const KINDS = [
4845
5825
  "would-stop-workspace",
4846
5826
  "would-remove-worktree",
4847
5827
  "would-remove-orphan",
4848
- "would-unregister-cache"
5828
+ "would-unregister-cache",
5829
+ "would-delete-parked-device",
5830
+ "would-delete-devices",
5831
+ "would-clear-build-dirs",
5832
+ "would-delete-dependencies",
5833
+ "would-stop-gradle-daemons",
5834
+ "would-delete-gradle-version",
5835
+ "would-delete-temp-leftovers"
4849
5836
  ];
4850
5837
  /** Rejects missing, corrupt or unsupported payloads without throwing. */
4851
5838
  function parseMaintenanceState(value) {
@@ -4859,13 +5846,14 @@ function parseMaintenanceState(value) {
4859
5846
  "metro-cache",
4860
5847
  "compilation-cache",
4861
5848
  "ccache",
5849
+ "owned-devices",
4862
5850
  "other"
4863
5851
  ].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
5852
  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
5853
  "worktree",
4866
5854
  "sweep",
4867
5855
  "disk"
4868
- ].includes(String(action.check))))) return null;
5856
+ ].includes(String(action.check))) && (action.level === void 0 || action.level === 2 || action.level === 3))) return null;
4869
5857
  if (!Array.isArray(value.recent) || !value.recent.every((record) => isJsonObject(record) && record.src === "maintenance" && finite(record.ts) && typeof record.msg === "string" && [
4870
5858
  "debug",
4871
5859
  "info",
@@ -4881,7 +5869,12 @@ function parseMaintenanceState(value) {
4881
5869
  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
5870
  if (value.pressure !== null) {
4883
5871
  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) || ![
5872
+ 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 && ![
5873
+ 0,
5874
+ 1,
5875
+ 2,
5876
+ 3
5877
+ ].some((level) => level === pressure.diskLevel) || !isJsonObject(pressure.memory) || ![
4885
5878
  null,
4886
5879
  "normal",
4887
5880
  "warning",
@@ -4899,8 +5892,8 @@ function readMaintenanceAttempt() {
4899
5892
  }
4900
5893
  //#endregion
4901
5894
  //#region state/agent-device.ts
4902
- function agentDeviceStateDirs(home = homedir()) {
4903
- const override = process.env.AGENT_DEVICE_STATE_DIR?.trim();
5895
+ function agentDeviceStateDirs(home = ambientEnv().HOME || ambientEnv().USERPROFILE || homedir(), env = ambientEnv()) {
5896
+ const override = env.AGENT_DEVICE_STATE_DIR?.trim();
4904
5897
  const dirs = /* @__PURE__ */ new Set([override ? resolve(override) : join(home, ".agent-device")]);
4905
5898
  const workspaces = join(configDir(), "workspaces");
4906
5899
  try {
@@ -4913,6 +5906,20 @@ function agentDeviceStateDirs(home = homedir()) {
4913
5906
  } catch {}
4914
5907
  return [...dirs];
4915
5908
  }
5909
+ async function agentDeviceStateDirsAsync(home = ambientEnv().HOME || ambientEnv().USERPROFILE || homedir(), env = ambientEnv()) {
5910
+ const override = env.AGENT_DEVICE_STATE_DIR?.trim();
5911
+ const dirs = /* @__PURE__ */ new Set([override ? resolve(override) : join(home, ".agent-device")]);
5912
+ const workspaces = join(configDir(), "workspaces");
5913
+ try {
5914
+ for (const name of await readdir(workspaces)) {
5915
+ const dir = join(workspaces, name, "agent-device");
5916
+ try {
5917
+ if ((await stat(dir)).isDirectory()) dirs.add(dir);
5918
+ } catch {}
5919
+ }
5920
+ } catch {}
5921
+ return [...dirs];
5922
+ }
4916
5923
  //#endregion
4917
5924
  //#region state/archive.ts
4918
5925
  function readArchives() {
@@ -4925,7 +5932,29 @@ function readArchives() {
4925
5932
  return names.flatMap((name) => {
4926
5933
  const archive = readArchive(name);
4927
5934
  return archive ? [archive] : [];
4928
- }).toSorted((a, b) => Date.parse(b.removedAt) - Date.parse(a.removedAt) || b.id.localeCompare(a.id));
5935
+ }).toSorted(archiveOrder);
5936
+ }
5937
+ function archiveOrder(a, b) {
5938
+ return Date.parse(b.removedAt) - Date.parse(a.removedAt) || b.id.localeCompare(a.id);
5939
+ }
5940
+ /** Reads current archive files asynchronously, outside the synchronous state-read cache; cancellation propagates. */
5941
+ async function readArchivesAsync(signal) {
5942
+ signal?.throwIfAborted();
5943
+ let names;
5944
+ try {
5945
+ names = await readdir(archiveRoot());
5946
+ } catch {
5947
+ signal?.throwIfAborted();
5948
+ return [];
5949
+ }
5950
+ const archives = [];
5951
+ for (const name of names) {
5952
+ signal?.throwIfAborted();
5953
+ const archive = await readArchiveAsync(name, signal);
5954
+ if (archive) archives.push(archive);
5955
+ }
5956
+ signal?.throwIfAborted();
5957
+ return archives.toSorted(archiveOrder);
4929
5958
  }
4930
5959
  function readArchive(id) {
4931
5960
  if (!id || id.startsWith(".") || basename(id) !== id || id.includes("\0")) return null;
@@ -4935,8 +5964,33 @@ function readArchive(id) {
4935
5964
  } catch {
4936
5965
  return null;
4937
5966
  }
4938
- const record = readJsonObject(join(archiveRoot(), id, "archive.json"));
4939
- if (!record || record.id !== id || [
5967
+ return archiveRecord(id, readJsonObject(join(archiveRoot(), id, "archive.json")));
5968
+ }
5969
+ /** Reads a current archive without the synchronous state-read cache; invalid or missing records return null. */
5970
+ async function readArchiveAsync(id, signal) {
5971
+ signal?.throwIfAborted();
5972
+ if (!id || id.startsWith(".") || basename(id) !== id || id.includes("\0")) return null;
5973
+ const dir = join(archiveRoot(), id);
5974
+ try {
5975
+ const directory = await lstat(dir);
5976
+ signal?.throwIfAborted();
5977
+ if (!directory.isDirectory()) return null;
5978
+ const file = await lstat(join(dir, "archive.json"));
5979
+ signal?.throwIfAborted();
5980
+ if (!file.isFile()) return null;
5981
+ const text = await readFile(join(dir, "archive.json"), {
5982
+ encoding: "utf8",
5983
+ signal
5984
+ });
5985
+ signal?.throwIfAborted();
5986
+ return archiveRecord(id, JSON.parse(text));
5987
+ } catch {
5988
+ signal?.throwIfAborted();
5989
+ return null;
5990
+ }
5991
+ }
5992
+ function archiveRecord(id, record) {
5993
+ if (!isJsonObject(record) || record.id !== id || [
4940
5994
  "projectRoot",
4941
5995
  "project",
4942
5996
  "workspace",
@@ -5024,14 +6078,13 @@ const knownKeys = (value, keys) => Object.keys(value).every((key) => keys.includ
5024
6078
  function parseSetupJournal(value) {
5025
6079
  if (!isJsonObject(value) || !knownKeys(value, [
5026
6080
  "v",
5027
- "client",
5028
6081
  "expiresAt",
5029
6082
  "capabilities",
5030
6083
  "steps",
5031
6084
  "granted",
5032
6085
  "done",
5033
6086
  "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, [
6087
+ ]) || 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
6088
  "id",
5036
6089
  "state",
5037
6090
  "title",
@@ -5057,4 +6110,4 @@ function isSetupJournalPrunable(journal, now) {
5057
6110
  return Date.parse(journal.expiresAt) + 36e5 < now;
5058
6111
  }
5059
6112
  //#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 };
6113
+ 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, listRecordedDevicesAsync, listSegments, listSegmentsAsync, 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, readArchiveAsync, readArchiveState, readArchives, readArchivesAsync, 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 };