peachsim 0.1.625 → 0.1.627

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 (82) hide show
  1. package/README.md +3 -2
  2. package/cli/commands/box/bundle-plane.ts +7 -2
  3. package/cli/commands/box.ts +9 -2
  4. package/cli/commands/control.ts +32 -1
  5. package/cli/commands/flow.ts +17 -6
  6. package/cli/commands/maestro-generate.ts +5 -12
  7. package/cli/commands/maestro.ts +16 -6
  8. package/cli/commands/platform.ts +1 -0
  9. package/cli/commands/test.ts +14 -0
  10. package/cli/commands/upload.ts +18 -8
  11. package/cli/drivers/playwright-sim-host.ts +10 -3
  12. package/cli/drivers/playwright.ts +8 -5
  13. package/cli/run-rnx.ts +8 -4
  14. package/dist-lib/agent-daemon-client.cjs +2 -1
  15. package/dist-lib/agent-events.cjs +1 -1
  16. package/dist-lib/agent-identity.cjs +1 -1
  17. package/dist-lib/agent-sessions.cjs +1 -1
  18. package/dist-lib/attached-projects.cjs +1 -1
  19. package/dist-lib/auth/shared-session.cjs +1 -1
  20. package/dist-lib/backend-origin.cjs +1 -1
  21. package/dist-lib/beta.cjs +1 -1
  22. package/dist-lib/beta.mjs +1 -1
  23. package/dist-lib/bridge-constants.cjs +4 -1
  24. package/dist-lib/bridge-contract-input.cjs +1 -1
  25. package/dist-lib/bridge-contract-input.mjs +1 -1
  26. package/dist-lib/bridge-contract.cjs +1 -1
  27. package/dist-lib/bridge-contract.mjs +1 -1
  28. package/dist-lib/capture-contract.cjs +18 -8
  29. package/dist-lib/capture-contract.mjs +18 -8
  30. package/dist-lib/cli-constants.cjs +1 -1
  31. package/dist-lib/cloud-contract.cjs +1 -1
  32. package/dist-lib/cloud-contract.mjs +1 -1
  33. package/dist-lib/cloud-test-args.cjs +1 -1
  34. package/dist-lib/cloud-test-args.mjs +1 -1
  35. package/dist-lib/cloud.cjs +1 -1
  36. package/dist-lib/cloud.mjs +1 -1
  37. package/dist-lib/config.cjs +1 -1
  38. package/dist-lib/detox/index.cjs +1 -1
  39. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  40. package/dist-lib/home-paths.cjs +1 -1
  41. package/dist-lib/host/bridge-host.cjs +239 -102
  42. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  43. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  44. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  45. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  46. package/dist-lib/host/websocket-proxy.cjs +1 -1
  47. package/dist-lib/index.cjs +19 -8
  48. package/dist-lib/jump-to-source-babel.cjs +1 -1
  49. package/dist-lib/jump-to-source-native.cjs +1 -1
  50. package/dist-lib/menu.cjs +1 -1
  51. package/dist-lib/menu.mjs +1 -1
  52. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  53. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  54. package/dist-lib/metro-production-bundle.cjs +1 -1
  55. package/dist-lib/metro-production-bundle.mjs +1 -1
  56. package/dist-lib/metro.cjs +1 -1
  57. package/dist-lib/profiles.cjs +1 -1
  58. package/dist-lib/public-brand.cjs +1 -1
  59. package/dist-lib/react-native-host-modules.cjs +1 -1
  60. package/dist-lib/react-native-host-modules.mjs +1 -1
  61. package/dist-lib/render-mode.cjs +1 -1
  62. package/dist-lib/scripts/dev-server-scanner.cjs +215 -28
  63. package/dist-lib/sdk.cjs +1 -1
  64. package/dist-lib/sdk.mjs +1 -1
  65. package/dist-lib/skills.cjs +292 -128
  66. package/dist-lib/swift-compile.cjs +1 -1
  67. package/dist-lib/swift-compile.mjs +1 -1
  68. package/dist-lib/swift-package.cjs +1 -1
  69. package/dist-lib/swift-package.mjs +1 -1
  70. package/dist-lib/swift-submit.cjs +1 -1
  71. package/dist-lib/swift-submit.mjs +1 -1
  72. package/dist-lib/swift.cjs +14488 -14349
  73. package/dist-lib/vite-kotlin.cjs +1 -1
  74. package/dist-lib/vite-swift.cjs +1 -1
  75. package/dist-lib/vite.cjs +2 -1
  76. package/install.mjs +16 -0
  77. package/install.sh +199 -0
  78. package/package.json +6 -1
  79. package/scripts/dev-server-scanner.ts +77 -46
  80. package/src/bridge-constants.ts +11 -0
  81. package/src/capture-contract.ts +40 -7
  82. package/src/host/bridge-host.ts +24 -103
@@ -9,6 +9,7 @@ import {
9
9
  } from '../../scripts/dev-server-scanner.ts'
10
10
  import { buildBrowserAssetProxyUrl } from '../backend-origin.ts'
11
11
  import {
12
+ AUTOMATION_SIM_IDLE_REAP_TTL_MS,
12
13
  DEFAULT_SOOTSIM_BRIDGE_PORT,
13
14
  SOOTSIM_BRIDGE_SIM_CLOSE_CODE,
14
15
  SOOTSIM_BRIDGE_SIM_CLOSE_REASON,
@@ -260,23 +261,6 @@ export interface BridgeHostOptions {
260
261
  * `peach serve` command resolves this with the same probe the
261
262
  * upload itself uses (`resolveDefaultUploadOrigin`). */
262
263
  contrastOrigin?: string
263
- /** override the abandoned-sim GC TTL (ms). defaults to
264
- * SIM_IDLE_REAP_TTL_MS (30 min). exists so the reaper integration test
265
- * can drive the real timer path deterministically instead of mocking
266
- * time. production callers should never set this. */
267
- simIdleReapTtlMs?: number
268
- /** override the idle GC TTL (ms) for automation sims (playwright-driver,
269
- * capture/factory). defaults to AUTOMATION_SIM_IDLE_REAP_TTL_MS (10 min).
270
- * test-only, like simIdleReapTtlMs. */
271
- automationSimIdleReapTtlMs?: number
272
- /** cap on concurrent non-primary automation sims before the reaper drops
273
- * the most-idle overflow. defaults to MAX_CONCURRENT_AUTOMATION_SIMS (6).
274
- * test-only. */
275
- maxConcurrentAutomationSims?: number
276
- /** grace (ms) below which an automation sim is never reaped by the cap,
277
- * even when over it. defaults to AUTOMATION_SIM_ACTIVE_GRACE_MS (60 s).
278
- * test-only. */
279
- automationSimActiveGraceMs?: number
280
264
  }
281
265
 
282
266
  const DAEMON_HEARTBEAT_INTERVAL_MS = 5_000
@@ -417,25 +401,9 @@ export class SootSimBridgeHost {
417
401
  // terminal 4001 code, so the client closes the abandoned window instead
418
402
  // of reconnecting the zombie straight back in.
419
403
  private static SIM_IDLE_REAP_TTL_MS = 30 * 60_000
420
- // automation sims (spawned by the playwright driver: `peach open --driver
421
- // playwright`, the factory/screenshot capture path — `meta.sootsimHostDriver
422
- // === 'playwright'`) are unattended one-shot browsers, never a human's tab. a
423
- // capture run opens one per slide/app/retry and the 30-min human/dev TTL lets
424
- // hundreds pile up inside the window — orphaned ms-playwright chromiums that
425
- // drove the box to swap-thrash (load hit 157 in one incident). give them a far
426
- // shorter idle TTL, and cap how many can sit concurrently: reap the most-idle
427
- // overflow, but never a sim active within ACTIVE_GRACE (an agent composing
428
- // between captures), nor primary / cli-attached / leased sims.
429
- private static AUTOMATION_SIM_IDLE_REAP_TTL_MS = 10 * 60_000
430
- private static MAX_CONCURRENT_AUTOMATION_SIMS = 6
431
- private static AUTOMATION_SIM_ACTIVE_GRACE_MS = 60_000
432
404
 
433
405
  private preferredPort: number
434
406
  private portFallbackCount: number
435
- private simIdleReapTtlMs: number
436
- private automationSimIdleReapTtlMs: number
437
- private maxConcurrentAutomationSims: number
438
- private automationSimActiveGraceMs: number
439
407
  private shouldWriteLockfile: boolean
440
408
  private shouldWriteDevLockfile: boolean
441
409
  private getShellPort: (() => number | null) | null
@@ -486,14 +454,6 @@ export class SootSimBridgeHost {
486
454
  getExcludePorts: opts.agentScanExcludes,
487
455
  })
488
456
  this.contrastOrigin = opts.contrastOrigin?.replace(/\/$/, '') || null
489
- this.simIdleReapTtlMs =
490
- opts.simIdleReapTtlMs ?? SootSimBridgeHost.SIM_IDLE_REAP_TTL_MS
491
- this.automationSimIdleReapTtlMs =
492
- opts.automationSimIdleReapTtlMs ?? SootSimBridgeHost.AUTOMATION_SIM_IDLE_REAP_TTL_MS
493
- this.maxConcurrentAutomationSims =
494
- opts.maxConcurrentAutomationSims ?? SootSimBridgeHost.MAX_CONCURRENT_AUTOMATION_SIMS
495
- this.automationSimActiveGraceMs =
496
- opts.automationSimActiveGraceMs ?? SootSimBridgeHost.AUTOMATION_SIM_ACTIVE_GRACE_MS
497
457
  }
498
458
 
499
459
  /** expose the agent host so tests and embedders can inspect state or
@@ -502,14 +462,6 @@ export class SootSimBridgeHost {
502
462
  return this.agentHost
503
463
  }
504
464
 
505
- /** run the abandoned-sim GC pass on demand. the reaper normally fires off
506
- * the 30s idle-sweep timer; this lets the F21-3 integration test drive
507
- * the real path without waiting for the timer. not part of the public
508
- * WS protocol. */
509
- reapIdleSimsForTest(now = Date.now()): void {
510
- this.reapIdleSims(now)
511
- }
512
-
513
465
  /** synchronous wrapper around startAsync for callers that don't care
514
466
  * about port fallback outcomes. returns immediately; actual binding
515
467
  * happens on the event loop. callers that need to know the bound port
@@ -816,6 +768,13 @@ export class SootSimBridgeHost {
816
768
  return
817
769
  }
818
770
 
771
+ // a capture the page is still running after its cli exited (`peach
772
+ // record start`) is use the idle reaper cannot see in cli traffic.
773
+ if (msg.type === 'bridge:capture-active' && sim) {
774
+ sim.lastActiveAt = Date.now()
775
+ return
776
+ }
777
+
819
778
  // write a partial patch into ~/.peach/config.json. fired by the
820
779
  // engine's settingsStore when a persisted key flips in a browser
821
780
  // tab (which has no fs access of its own). after the merge, we
@@ -2025,71 +1984,33 @@ export class SootSimBridgeHost {
2025
1984
  // automation sims are spawned by the playwright driver (`peach open
2026
1985
  // --driver playwright`, the factory/screenshot capture path), which tags them
2027
1986
  // with `meta.sootsimHostDriver === 'playwright'` via the host's addInitScript.
2028
- // they are unattended one-shot browsers, never a human's tab, so they get a
2029
- // shorter idle TTL and a concurrent cap.
2030
1987
  private isAutomationSim(sim: BridgeSimConnection): boolean {
2031
1988
  return sim.meta?.sootsimHostDriver === 'playwright'
2032
1989
  }
2033
1990
 
2034
- // an automation sim whose spawning playwright host process is still alive is
2035
- // an ACTIVE run, not an abandoned tab. mirrors the cli's playwrightHostPidForSim
2036
- // (control.ts): extract the host pid from meta, then check liveness so the
2037
- // host-side idle GC never kills a live capture/factory sim mid-run.
2038
- private automationSimOwnerAlive(sim: BridgeSimConnection): boolean {
2039
- const pid = Number(sim.meta?.sootsimHostPid)
2040
- if (!Number.isInteger(pid) || pid <= 1 || pid === process.pid) return false
2041
- try {
2042
- process.kill(pid, 0) // ESRCH → dead; EPERM → alive but unsignalable
2043
- return true
2044
- } catch (err) {
2045
- return (err as NodeJS.ErrnoException).code === 'EPERM'
2046
- }
2047
- }
2048
-
2049
1991
  private reapIdleSims(now = Date.now()) {
2050
1992
  const attachedSimIds = new Set(this.cliSimBySocket.values())
2051
- // automation sims that pass every "someone cares" gate but are still under
2052
- // their TTL — candidates for the concurrent-cap pass below.
2053
- const automationLive: { sim: BridgeSimConnection; idleMs: number }[] = []
2054
1993
  for (const sim of this.sims.values()) {
2055
- if (sim.id === this.primarySimId) continue
2056
1994
  if (attachedSimIds.has(sim.id)) continue
2057
- if (this.getActiveLease(sim)) continue
2058
- // never-CLI-driven sims have lastActiveAt 0 — fall back to connectedAt
2059
- // so a freshly opened, not-yet-primary tab isn't reaped on the spot.
2060
- const lastTouched = Math.max(sim.lastActiveAt, sim.connectedAt)
2061
- const idleMs = now - lastTouched
2062
1995
  const automation = this.isAutomationSim(sim)
2063
- // a live-owner automation sim is an ACTIVE run (its spawning playwright host
2064
- // process is still alive) — never reap it. the host keeps Chrome alive
2065
- // intentionally and tears it down itself when ITS owner driver dies; only
2066
- // orphaned/host-dead autos are the leak this reaper targets. (2026-06: the
2067
- // 10-min automation TTL was reaping live capture + factory sims mid-run —
2068
- // too aggressive.) continue skips both the TTL reap and the cap pass below.
2069
- if (automation && this.automationSimOwnerAlive(sim)) continue
2070
- const ttl = automation ? this.automationSimIdleReapTtlMs : this.simIdleReapTtlMs
2071
- if (idleMs >= ttl) {
2072
- // terminal close → client closes the window, does not reconnect; the
2073
- // 'close' handler runs rememberDisconnectedSim → this.sims.delete.
2074
- this.closeSimSocketFromHost(sim.ws)
1996
+ const lease = this.getActiveLease(sim)
1997
+ // an automation sim is nobody's tab, so being primary or holding a cli
1998
+ // lease (10 min from its last grant) says nothing about use: only a human
1999
+ // interacting with a headed window keeps one past its idle window.
2000
+ if (
2001
+ automation ? lease?.kind === 'user-active' : sim.id === this.primarySimId || lease
2002
+ ) {
2075
2003
  continue
2076
2004
  }
2077
- if (automation) automationLive.push({ sim, idleMs })
2078
- }
2079
- // concurrent cap: a burst of capture opens can pile up hundreds inside the
2080
- // TTL window. keep at most maxConcurrentAutomationSims of the freshest;
2081
- // reap the most-idle overflow, but never one active within the grace (an
2082
- // agent mid-capture between screenshots).
2083
- const cap = this.maxConcurrentAutomationSims
2084
- if (automationLive.length > cap) {
2085
- automationLive.sort((a, b) => b.idleMs - a.idleMs) // most-idle first
2086
- for (const { sim, idleMs } of automationLive.slice(
2087
- 0,
2088
- automationLive.length - cap,
2089
- )) {
2090
- if (idleMs < this.automationSimActiveGraceMs) continue
2091
- this.closeSimSocketFromHost(sim.ws)
2092
- }
2005
+ // never-CLI-driven sims have lastActiveAt 0 — fall back to connectedAt
2006
+ // so a freshly opened, not-yet-primary tab isn't reaped on the spot.
2007
+ const idleMs = now - Math.max(sim.lastActiveAt, sim.connectedAt)
2008
+ const ttl = automation
2009
+ ? AUTOMATION_SIM_IDLE_REAP_TTL_MS
2010
+ : SootSimBridgeHost.SIM_IDLE_REAP_TTL_MS
2011
+ // terminal close → client closes the window, does not reconnect; the
2012
+ // 'close' handler runs rememberDisconnectedSim → this.sims.delete.
2013
+ if (idleMs >= ttl) this.closeSimSocketFromHost(sim.ws)
2093
2014
  }
2094
2015
  }
2095
2016