humanish 0.136.1 → 0.138.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 (99) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/actors/computer-use/actor.d.ts +2 -0
  3. package/dist/actors/computer-use/actor.js +1 -0
  4. package/dist/actors/computer-use/loop/observation.js +2 -0
  5. package/dist/actors/computer-use/loop/session.d.ts +3 -2
  6. package/dist/actors/computer-use/loop/session.js +8 -12
  7. package/dist/actors/computer-use/loop/types.d.ts +14 -1
  8. package/dist/cli/commands/runs.js +2 -2
  9. package/dist/cli/doctor.js +24 -0
  10. package/dist/evidence/browser-network.d.ts +108 -0
  11. package/dist/evidence/browser-network.js +516 -0
  12. package/dist/evidence/trace-page.d.ts +31 -1
  13. package/dist/evidence/trace-page.js +68 -2
  14. package/dist/keys/key-resolution.js +53 -2
  15. package/dist/observer-app.html +2 -2
  16. package/dist/routes/computer-use/bundle.js +1 -0
  17. package/dist/routes/computer-use/e2b-desktop/desktop.js +6 -0
  18. package/dist/routes/computer-use/e2b-desktop/fidelity.d.ts +12 -1
  19. package/dist/routes/computer-use/e2b-desktop/fidelity.js +7 -2
  20. package/dist/routes/computer-use/e2b-desktop/network.d.ts +52 -0
  21. package/dist/routes/computer-use/e2b-desktop/network.js +153 -0
  22. package/dist/routes/computer-use/e2b-desktop/start.js +18 -1
  23. package/dist/routes/computer-use/e2b-desktop/state.d.ts +3 -0
  24. package/dist/routes/computer-use/e2b-desktop/state.js +2 -0
  25. package/dist/routes/computer-use/e2b-desktop/teardown.js +6 -3
  26. package/dist/routes/computer-use/fanout-records.js +10 -0
  27. package/dist/routes/computer-use/participant-desktop.d.ts +7 -2
  28. package/dist/routes/computer-use/participant-model.js +2 -1
  29. package/dist/routes/computer-use/plan.js +2 -1
  30. package/dist/routes/computer-use/single-bundle.d.ts +3 -0
  31. package/dist/routes/computer-use/single-bundle.js +4 -0
  32. package/dist/routes/computer-use/types.d.ts +7 -0
  33. package/dist/routes/scripted/plan.js +2 -2
  34. package/dist/routes/shared-world/plan.js +3 -2
  35. package/dist/routes/terminal/live-sandbox.d.ts +2 -0
  36. package/dist/routes/terminal/live-sandbox.js +8 -3
  37. package/dist/routes/terminal/plan.js +51 -8
  38. package/dist/routes/terminal/session.js +8 -1
  39. package/dist/run/bundle.d.ts +2 -1
  40. package/dist/run/cost-summary.js +9 -4
  41. package/dist/run/live-items.d.ts +20 -2
  42. package/dist/run/live-items.js +23 -14
  43. package/dist/run/pricing.d.ts +11 -0
  44. package/dist/run/pricing.js +18 -5
  45. package/dist/run/reclaim-outcomes.d.ts +4 -1
  46. package/dist/run/reclaim-outcomes.js +8 -6
  47. package/dist/run/reclaim.d.ts +4 -1
  48. package/dist/run/reclaim.js +81 -34
  49. package/dist/run/sandbox-creates.d.ts +7 -4
  50. package/dist/run/sandbox-creates.js +17 -9
  51. package/dist/run/sandbox-receipts.d.ts +13 -5
  52. package/dist/run/sandbox-receipts.js +18 -6
  53. package/dist/run/streams.d.ts +7 -0
  54. package/dist/study/composition-rules.js +11 -10
  55. package/dist/study/config.js +4 -1
  56. package/dist/study/doctor.js +1 -1
  57. package/dist/study/keys.d.ts +3 -0
  58. package/dist/study/keys.js +2 -0
  59. package/dist/study/parse/comms.js +1 -20
  60. package/dist/study/parse/execution.js +35 -3
  61. package/dist/study/parse/values.d.ts +5 -0
  62. package/dist/study/parse/values.js +22 -0
  63. package/dist/study/plan-base.js +6 -1
  64. package/dist/study/plan-types.d.ts +5 -2
  65. package/dist/study/requirements.js +7 -2
  66. package/dist/study/routing.d.ts +1 -1
  67. package/dist/study/routing.js +1 -1
  68. package/dist/study/study-fields.d.ts +7 -1
  69. package/dist/study/study-fields.js +10 -0
  70. package/dist/study/types.d.ts +12 -1
  71. package/dist/study/validation.d.ts +8 -0
  72. package/dist/study/validation.js +31 -2
  73. package/dist/substrates/detached.d.ts +5 -0
  74. package/dist/substrates/detached.js +9 -0
  75. package/dist/substrates/e2b/cdp-network-capture.d.ts +23 -0
  76. package/dist/substrates/e2b/cdp-network-capture.js +194 -0
  77. package/dist/substrates/e2b/cdp-probe.d.ts +20 -5
  78. package/dist/substrates/e2b/cdp-probe.js +98 -34
  79. package/dist/substrates/e2b/desktop-cdp.d.ts +32 -2
  80. package/dist/substrates/e2b/desktop-cdp.js +87 -5
  81. package/dist/substrates/e2b/desktop-resources.d.ts +4 -2
  82. package/dist/substrates/e2b/desktop-resources.js +2 -2
  83. package/dist/substrates/e2b/peer-install.js +1 -1
  84. package/dist/substrates/e2b/sandbox.d.ts +3 -0
  85. package/dist/substrates/e2b/sandbox.js +5 -4
  86. package/dist/substrates/modal/image.d.ts +9 -0
  87. package/dist/substrates/modal/image.js +36 -0
  88. package/dist/substrates/modal/sandbox.d.ts +26 -0
  89. package/dist/substrates/modal/sandbox.js +367 -0
  90. package/dist/substrates/modal/sdk.d.ts +112 -0
  91. package/dist/substrates/modal/sdk.js +61 -0
  92. package/dist/verify/network.d.ts +10 -0
  93. package/dist/verify/network.js +162 -0
  94. package/dist/verify/pages.js +1 -16
  95. package/dist/verify/verify.js +8 -2
  96. package/docs/architecture/terminal-product-route.md +1 -1
  97. package/docs/contracts/run-bundle.md +90 -3
  98. package/docs/contracts/schemas.md +6 -5
  99. package/package.json +8 -2
package/CHANGELOG.md CHANGED
@@ -8,6 +8,29 @@ The Unreleased section holds the full notes for the next version until it is tag
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 0.138.0: A terminal study can run on Modal (2026-10-19)
12
+
13
+ humanish 0.138.0 can run a terminal study's shell on Modal as well as E2B. Set
14
+ `execution.target: modal-terminal` and `execution.runtimeAuth: openai-env`, sign in with
15
+ `modal token new`, and install the optional `modal` package beside humanish. The sandbox runs as a
16
+ non-root user with sudo, as on E2B, lives up to Modal's 24 hours, is journaled and reclaimed by id
17
+ like an E2B sandbox, and is priced at Modal's sandbox rate. Desktop studies still run on E2B or a
18
+ local VM.
19
+
20
+ [Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.138.0)
21
+
22
+ ## 0.137.0: Each browser participant can record its requests (2026-10-16)
23
+
24
+ humanish 0.137.0 can record the requests each hosted browser participant makes. With
25
+ `execution.network: true`, a computer-use participant in Chrome or Chromium writes
26
+ `network/<participant id>.ndjson`: one line per request hop with its turn, method, origin and
27
+ path, status, duration, and whether it failed or was still open. A 500 on add-to-cart or a list
28
+ call that never returns now sits next to the turn that started it. The file holds no headers,
29
+ cookies, bodies or query strings, verify checks every record against the contract, and capture is
30
+ off unless a study turns it on.
31
+
32
+ [Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.137.0)
33
+
11
34
  ## 0.136.1: The emulation log no longer holds page addresses (2026-10-16)
12
35
 
13
36
  humanish 0.136.1 keeps page addresses out of a mobile-emulated participant's emulation log. The
@@ -80,6 +80,8 @@ export interface CuaActorSessionOptions {
80
80
  onScreenshot?: (frame: Buffer) => void;
81
81
  /** Whether the trace carries the observed page, threaded to the loop; see LoopRunOptions.recordPages. */
82
82
  recordPages?: boolean;
83
+ /** Runtime-only checkpoint callback threaded to the loop; see LoopRunOptions.onCheckpoint. */
84
+ onCheckpoint?: (turnNumber: number) => void;
83
85
  /** Per-turn trace snapshot callback threaded to the loop; see CuaLoopOptions.onTrace. */
84
86
  onTrace?: (items: readonly ActorTraceItem[], usage: ActorTokenUsage, metadata?: CuaLiveMetadata) => void;
85
87
  }
@@ -58,6 +58,7 @@ export async function runCuaActorSession(options) {
58
58
  ...(options.onScreenshot === undefined ? {} : { onScreenshot: options.onScreenshot }),
59
59
  ...(options.onTrace === undefined ? {} : { onTrace: options.onTrace }),
60
60
  ...(options.recordPages === undefined ? {} : { recordPages: options.recordPages }),
61
+ ...(options.onCheckpoint === undefined ? {} : { onCheckpoint: options.onCheckpoint }),
61
62
  };
62
63
  const result = await runComputerUseLoopWithTaps(loopOptions);
63
64
  if (result.trace.executionProfile?.billing === "account-unknown") {
@@ -51,6 +51,7 @@ export class DesktopObserver {
51
51
  if (frameless !== undefined)
52
52
  return { stop: frameless };
53
53
  await this.recordScreenshot(observation, opening ? "turn-00-start" : `turn-${pad(turnNumber)}`);
54
+ session.settings.onCheckpoint?.(turnNumber);
54
55
  session.flush();
55
56
  this.observeTasks(observation, turnNumber);
56
57
  const dwell = await this.dwellIfDue(observation, turnNumber);
@@ -58,6 +59,7 @@ export class DesktopObserver {
58
59
  if (dwell !== undefined) {
59
60
  observation = this.handToHooks(this.withHeardSpeech(await this.observeBounded("after dwell")));
60
61
  await this.recordScreenshot(observation, `turn-${pad(turnNumber)}-after-dwell`);
62
+ session.settings.onCheckpoint?.(turnNumber);
61
63
  this.observeTasks(observation, turnNumber);
62
64
  if (dwell.next === "stop") {
63
65
  const when = opening ? "at the start" : `after turn ${turnNumber}`;
@@ -13,7 +13,7 @@ export type ScreenshotRef = NonNullable<ActorTraceItem["screenshotRef"]>;
13
13
  * wrapped callbacks are fields on LoopSession instead. Callers destructure hooks to call them.
14
14
  */
15
15
  export type LoopSettings = {
16
- readonly [K in "instructions" | "persona" | "redaction" | "stopWhen" | "dwell" | "clock" | "tasks" | "maxUsd" | "overRunBudget" | "estimateTurnCostUsd" | "onObservedUrl" | "onMessage" | "onScreenshot" | "onTrace" | "recordPages"]: LoopRunOptions[K];
16
+ readonly [K in "instructions" | "persona" | "redaction" | "stopWhen" | "dwell" | "clock" | "tasks" | "maxUsd" | "overRunBudget" | "estimateTurnCostUsd" | "onObservedUrl" | "onMessage" | "onScreenshot" | "onTrace" | "recordPages" | "onCheckpoint"]: LoopRunOptions[K];
17
17
  };
18
18
  /** The participant's dispatched actions, as the backstop and failure notices describe them. */
19
19
  export interface ActionHistory {
@@ -72,9 +72,10 @@ export declare class LoopSession {
72
72
  /** Set by the participant's own final account or by its closing report. */
73
73
  impressions: ParticipantImpressions | undefined;
74
74
  private stopCause;
75
- private readonly scrubText;
75
+ private readonly redactTrace;
76
76
  constructor(options: LoopRunOptions);
77
77
  remaining(): number;
78
+ /** Model-authored narration and recorded pages, through traceRedaction. */
78
79
  redactNarration(text: string): string;
79
80
  /** Hand a watcher the trace so far with the running usage, without waiting on it. */
80
81
  flush(): void;
@@ -1,4 +1,4 @@
1
- import { redactEncodedText } from "../../../evidence/encoded-text.js";
1
+ import { traceRedaction } from "../../../evidence/trace-page.js";
2
2
  import { TaskTracker } from "../../../study/tasks.js";
3
3
  import { StreakRecord } from "./streak.js";
4
4
  import { TraceRecorder } from "./trace.js";
@@ -73,10 +73,10 @@ export class LoopSession {
73
73
  /** Set by the participant's own final account or by its closing report. */
74
74
  impressions;
75
75
  stopCause;
76
- scrubText;
76
+ redactTrace;
77
77
  constructor(options) {
78
78
  // Read once, in this order: a caller mutating its options object mid-run changes nothing.
79
- const { instructions, provider, executor, persona, redaction, timeoutMs, now, signal, idleSteps = DEFAULT_IDLE_STEPS, noProgressSteps = DEFAULT_NO_PROGRESS_STEPS, maxWaitMs, idleWaitMs, acknowledgeSafetyChecks = () => null, redactScreenshots = false, scrubText = (text) => text, writeScreenshot = async (name) => `screenshots/${name}`, stopWhen, dwell, clock, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), tasks, maxUsd, overRunBudget, estimateTurnCostUsd, requireReportedUsageForSpendCap = false, onObservedUrl, onMessage, onScreenshot, onTrace, recordPages, } = options;
79
+ const { instructions, provider, executor, persona, redaction, timeoutMs, now, signal, idleSteps = DEFAULT_IDLE_STEPS, noProgressSteps = DEFAULT_NO_PROGRESS_STEPS, maxWaitMs, idleWaitMs, acknowledgeSafetyChecks = () => null, redactScreenshots = false, scrubText = (text) => text, writeScreenshot = async (name) => `screenshots/${name}`, stopWhen, dwell, clock, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), tasks, maxUsd, overRunBudget, estimateTurnCostUsd, requireReportedUsageForSpendCap = false, onObservedUrl, onMessage, onScreenshot, onTrace, recordPages, onCheckpoint, } = options;
80
80
  this.settings = {
81
81
  instructions,
82
82
  persona,
@@ -93,6 +93,7 @@ export class LoopSession {
93
93
  onScreenshot,
94
94
  onTrace,
95
95
  recordPages,
96
+ onCheckpoint,
96
97
  };
97
98
  this.provider = provider;
98
99
  this.executor = executor;
@@ -108,7 +109,8 @@ export class LoopSession {
108
109
  this.redactScreenshots = redactScreenshots;
109
110
  this.now = () => now();
110
111
  this.acknowledgeSafetyChecks = (checks) => acknowledgeSafetyChecks(checks);
111
- this.scrubText = (text) => scrubText(text);
112
+ // The run's scrubKnownValues comes in as scrubText.
113
+ this.redactTrace = traceRedaction((text) => scrubText(text), (text) => redaction.redactText(text));
112
114
  this.writeScreenshot = (name, bytes) => writeScreenshot(name, bytes);
113
115
  this.sleep = (ms) => sleep(ms);
114
116
  this.capDeclared = maxUsd !== undefined || overRunBudget !== undefined;
@@ -123,15 +125,9 @@ export class LoopSession {
123
125
  remaining() {
124
126
  return this.timeoutMs - (this.now() - this.startedAtMs);
125
127
  }
126
- // Model-authored narration: literal-scrub known provisioned values, then pattern-redact.
127
- // A value the model transcribes (a DB password it read on screen) has no shape, so redactText
128
- // alone cannot catch it; the run's scrubKnownValues, injected as scrubText, closes that. The
129
- // participant receives its instructions as written, so it can repeat a key the study wrote
130
- // base64- or percent-encoded, which verify decodes; redactEncodedText removes that one too.
131
- // The known values are scrubbed again last: a marker written before other text can spell one,
132
- // as `[REDACTED_SECRET] and` spells `SECRET] and`.
128
+ /** Model-authored narration and recorded pages, through traceRedaction. */
133
129
  redactNarration(text) {
134
- return this.scrubText(redactEncodedText(this.settings.redaction.redactText(this.scrubText(text))));
130
+ return this.redactTrace(text);
135
131
  }
136
132
  /** Hand a watcher the trace so far with the running usage, without waiting on it. */
137
133
  flush() {
@@ -484,7 +484,10 @@ export interface LoopTaps {
484
484
  */
485
485
  onScreenshot?: (frame: Buffer) => void;
486
486
  }
487
- /** The loop's options inside humanish: the public ones, the handoff's taps and the page switch. */
487
+ /**
488
+ * The loop's options inside humanish: the public ones, the handoff's taps, the page switch and the
489
+ * checkpoint tap.
490
+ */
488
491
  export type LoopRunOptions = CuaLoopOptions & LoopTaps & {
489
492
  /**
490
493
  * Whether screenshot items and the trace carry the observed page (`page`, `finalPage`).
@@ -492,6 +495,16 @@ export type LoopRunOptions = CuaLoopOptions & LoopTaps & {
492
495
  * as digests (docs/architecture/external-public-shared-world.md).
493
496
  */
494
497
  recordPages?: boolean;
498
+ /**
499
+ * Runtime-only checkpoint tap: called with the turn's number after its checkpoint screenshot
500
+ * item (turn-03) is recorded, and again after its after-dwell item (turn-03-after-dwell),
501
+ * before the turn's stop checks. A held turn and a reply cut off by the output limit take no
502
+ * checkpoint, so they never reach it. Default: no-op.
503
+ *
504
+ * Built-in caller: the hosted E2B desktop pairs the number with the time of the browser-state
505
+ * read that screenshot came after, and its network log numbers requests by those pairs.
506
+ */
507
+ onCheckpoint?: (turnNumber: number) => void;
495
508
  };
496
509
  export type CuaLiveMetadata = Pick<ActorTrace, "executionProfile" | "providerRequests" | "historyTurnsOmitted">;
497
510
  /** What runComputerUseLoop returns; the public name for an actor session result. */
@@ -203,12 +203,12 @@ export function registerRuntimeCommands(parent, io) {
203
203
  export function registerReclaimCommand(parent, io) {
204
204
  parent
205
205
  .command("reclaim")
206
- .description("Kill an interrupted run's sandboxes: the exact ids journaled in its sandbox-receipts.ndjson, and any sandbox E2B lists with this run's owner tags (a sandbox whose id never reached a receipt). It lists only sandboxes that match every tag of this run. --check asks E2B whether each still exists and kills nothing. Needs E2B_API_KEY in the environment or in --dotenv.")
206
+ .description("Kill an interrupted run's sandboxes: the exact ids journaled in its sandbox-receipts.ndjson, and any sandbox the run's provider lists with this run's owner tags (a sandbox whose id never reached a receipt). It lists only sandboxes that match every tag of this run. --check asks the provider whether each still exists and kills nothing. Needs E2B_API_KEY, or MODAL_TOKEN_ID and MODAL_TOKEN_SECRET for a run on Modal, in the environment or in --dotenv.")
207
207
  .summary("Stop or check an interrupted run's sandboxes.")
208
208
  .option("--cwd <path>", CWD_OPTION_DESCRIPTION, ".")
209
209
  .option("--run <id>", RUN_OPTION_DESCRIPTION, "latest")
210
210
  .addOption(new Option("--preflight", "Reclaim sandboxes left by interrupted `humanish study check` probes (journaled in .humanish/preflight) instead of a run's.").conflicts("run"))
211
- .option("--check", "Ask E2B whether each sandbox still exists; kill nothing and write nothing. Exits 0 only when all are gone.")
211
+ .option("--check", "Ask the provider whether each sandbox still exists; kill nothing and write nothing. Exits 0 only when all are gone.")
212
212
  .option("--dotenv <path>", DOTENV_OPTION_DESCRIPTION)
213
213
  .addOption(envFileAliasOption())
214
214
  .option("--json", JSON_OPTION_DESCRIPTION)
@@ -96,6 +96,7 @@ export async function doctor(cwdInput, options = {}) {
96
96
  const checks = [
97
97
  ...(await projectChecks(projectRoot)),
98
98
  await desktopSdkCheck(setup),
99
+ ...(setup?.keys.includes("MODAL_TOKEN_ID") === true ? [await modalSdkCheck()] : []),
99
100
  sandboxCeilingCheck(env),
100
101
  concurrentSandboxesCheck(env),
101
102
  terminalSurfaceCheck(),
@@ -239,6 +240,25 @@ async function desktopSdkCheck(setup) {
239
240
  : missingDesktopSdkMessage(humanishInstall()),
240
241
  };
241
242
  }
243
+ /** The optional `modal` peer, reported only for a study that runs on Modal, which needs it. */
244
+ async function modalSdkCheck() {
245
+ const present = await safeCheck(async () => {
246
+ try {
247
+ await import("modal");
248
+ return true;
249
+ }
250
+ catch {
251
+ return false;
252
+ }
253
+ });
254
+ return {
255
+ name: "modal sdk",
256
+ ok: present,
257
+ message: present
258
+ ? "optional peer modal is installed; provider access is not tested"
259
+ : `optional peer modal is absent, and this study runs on Modal. ${desktopPeerAdvice(humanishInstall(), "modal").advice}`,
260
+ };
261
+ }
242
262
  /**
243
263
  * The stakeholder surface. Reported as capability, never as a gate: the TUI is optional,
244
264
  * and `doctor` is itself mostly run by agents through a pipe, where a TTY requirement says
@@ -288,6 +308,10 @@ async function probeDoctorKeys(cwd, env, study, keyDeps, dotenv) {
288
308
  if (study) {
289
309
  const { resolveStudyManifest } = await import("../study/discover.js");
290
310
  const resolved = await resolveStudyManifest(cwd, study);
311
+ // A study on Modal reads the Modal token pair, which no other study needs.
312
+ if (resolved.ok && resolved.config.execution?.target === "modal-terminal")
313
+ for (const name of ["MODAL_TOKEN_ID", "MODAL_TOKEN_SECRET"])
314
+ keyNames.add(name);
291
315
  if (resolved.ok && resolved.config.comms?.email?.kind === "real") {
292
316
  const { receivingRequiredKey } = await import("../comms/setup.js");
293
317
  receivingKey = await receivingRequiredKey(cwd, resolved.config.comms.email.connection);
@@ -0,0 +1,108 @@
1
+ export declare const BROWSER_NETWORK_SCHEMA = "humanish.browser-network.v1";
2
+ /** Why records are missing from a participant's network log, as the index's `reason` says. */
3
+ export declare const BROWSER_NETWORK_GAPS: readonly ["no-capture", "holder-stopped", "capped", "not-collected"];
4
+ type BrowserNetworkGap = (typeof BROWSER_NETWORK_GAPS)[number];
5
+ /** A stream's `network` field in run.json. */
6
+ export interface BrowserNetworkIndex {
7
+ schema: typeof BROWSER_NETWORK_SCHEMA;
8
+ /**
9
+ * network/<participant id>.ndjson, with the file's record count, byte count and sha256, and how
10
+ * many of its records failed and are pending. All six are absent when no file was written:
11
+ * capture did not start (`no-capture`), the events were not read (`not-collected`), or capture
12
+ * started and kept no record, which `startedAt` tells apart.
13
+ */
14
+ path?: string;
15
+ records?: number;
16
+ /** Records with `failed`: requests that got no response or broke off. */
17
+ failed?: number;
18
+ /**
19
+ * Records with `pending`: requests with no recorded end, still open when the file was read or
20
+ * when capture stopped (`capped`, `holder-stopped`).
21
+ */
22
+ pending?: number;
23
+ bytes?: number;
24
+ sha256?: string;
25
+ /** When the launch page's Network domain answered, on the desktop's clock. */
26
+ startedAt?: string;
27
+ /** The origins whose requests are written, redacted as a trace page's origin is. */
28
+ origins: string[];
29
+ complete: boolean;
30
+ /** Present when `complete` is false. */
31
+ reason?: BrowserNetworkGap;
32
+ omitted: {
33
+ /** Requests to an origin outside `origins`. */
34
+ otherOrigin: number;
35
+ /**
36
+ * Records past the writer's caps, which are not checked. Requests after the holder's own cap
37
+ * (`capped` with no overCap) are not counted anywhere.
38
+ */
39
+ overCap: number;
40
+ /** Records that failed the record check. */
41
+ invalid: number;
42
+ };
43
+ }
44
+ /**
45
+ * A checkpoint screenshot's browser-state read: `wallTime` is the read's clock in seconds since the
46
+ * epoch on the desktop, the clock the holder's request times use, and `turn` the screenshot's N.
47
+ */
48
+ export interface TurnMark {
49
+ wallTime: number;
50
+ turn: number;
51
+ }
52
+ export interface BrowserNetworkCapture {
53
+ /** The participant's id, which names the file. */
54
+ participantId: string;
55
+ /** The origins whose requests are kept, as `new URL(url).origin` writes them. */
56
+ origins: readonly string[];
57
+ /** The holder's events file as the host read it, whole lines and possibly a last partial one. */
58
+ events: string;
59
+ /** The participant's checkpoint marks, in the order they were taken. */
60
+ marks: readonly TurnMark[];
61
+ /** The trace redaction the participant's pages went through (traceRedaction). */
62
+ redact: (text: string) => string;
63
+ /**
64
+ * The holder had exited before `events` was read. A holder killed by a signal writes no exit
65
+ * line, so without this the file reads as complete.
66
+ */
67
+ holderExited?: boolean;
68
+ /**
69
+ * The participant's own sandbox id and those the run journaled. Run.finish rewrites a journaled
70
+ * id in every run file after run.json indexes them, so the writer withholds each path segment
71
+ * that names one, before any cut, and the sweep finds nothing to change.
72
+ */
73
+ sandboxIds?: readonly string[];
74
+ }
75
+ export interface BrowserNetworkLog {
76
+ index: BrowserNetworkIndex;
77
+ /** The file's text, for `index.path`; absent when the index has no path. */
78
+ text?: string;
79
+ /** What a reader of the run should know about the log, such as records with no turn. */
80
+ warnings: string[];
81
+ }
82
+ /** Whether `value` is an ISO time as `Date.prototype.toISOString` writes one. */
83
+ export declare function isIsoTime(value: unknown): value is string;
84
+ /** The run-relative path of a participant's network log. */
85
+ export declare function networkLogPath(participantId: string): string;
86
+ /**
87
+ * What is wrong with one network log record, or undefined when it holds only the schema's keys and
88
+ * values. The writer leaves out a record this names; verify reports one as a finding. Never says
89
+ * what the record holds.
90
+ */
91
+ export declare function networkRecordProblem(record: unknown): string | undefined;
92
+ /**
93
+ * The index of a participant whose events were never read: capture did not start, or the file
94
+ * could not be read at teardown. No file goes with it.
95
+ */
96
+ export declare function networkIndexWithoutEvents(capture: Pick<BrowserNetworkCapture, "origins" | "redact" | "sandboxIds">, reason: "no-capture" | "not-collected"): BrowserNetworkIndex;
97
+ /**
98
+ * The network log of one participant: the records the holder's events give, joined by request id
99
+ * into one per hop, redacted, tagged with turns, checked and capped, then the index that binds
100
+ * them. A redirect gives the 3xx hop's record, then one with `redirect: true`. A request's turn is
101
+ * the N of the first mark at or after its start, or the last mark's N plus 1 after every mark.
102
+ * Capture that started and kept no record writes no file. Only a hop the caps still take is
103
+ * redacted, and the work yields to the event loop every 20 ms, so the other participants of a
104
+ * fan-out keep running while a 32 MiB events file is reduced. Throws only for a participant id
105
+ * that is not one path segment.
106
+ */
107
+ export declare function reduceBrowserNetwork(capture: BrowserNetworkCapture): Promise<BrowserNetworkLog>;
108
+ export {};