humanish 0.126.0 → 0.127.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 (51) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/actors/computer-use/loop/ending.d.ts +3 -1
  3. package/dist/actors/computer-use/loop/ending.js +5 -3
  4. package/dist/actors/computer-use/openai-provider.js +1 -1
  5. package/dist/actors/goal-source.d.ts +32 -2
  6. package/dist/actors/goal-source.js +41 -2
  7. package/dist/analysis/evidence-sources.js +2 -2
  8. package/dist/analysis/types.d.ts +2 -2
  9. package/dist/analysis/validation.js +2 -1
  10. package/dist/observer/data.js +8 -5
  11. package/dist/observer-app.html +1 -1
  12. package/dist/routes/computer-use/fanout-bundle.js +3 -0
  13. package/dist/routes/computer-use/fanout-records.js +1 -1
  14. package/dist/routes/computer-use/participant-execution.js +6 -2
  15. package/dist/routes/computer-use/participant-facts.js +4 -1
  16. package/dist/routes/computer-use/participant-runs.d.ts +9 -9
  17. package/dist/routes/computer-use/participant-runs.js +52 -86
  18. package/dist/routes/computer-use/plan.d.ts +2 -0
  19. package/dist/routes/computer-use/plan.js +26 -16
  20. package/dist/routes/computer-use/rerun-selection.js +8 -5
  21. package/dist/routes/computer-use/setup.js +1 -1
  22. package/dist/routes/computer-use/single-bundle.js +9 -1
  23. package/dist/routes/computer-use/types.d.ts +2 -3
  24. package/dist/routes/computer-use/types.js +0 -3
  25. package/dist/routes/shared-world/bundle-records.js +1 -1
  26. package/dist/routes/shared-world/external-public.js +1 -1
  27. package/dist/routes/shared-world/plan.d.ts +2 -0
  28. package/dist/routes/shared-world/plan.js +3 -3
  29. package/dist/routes/shared-world/provisioned.js +1 -1
  30. package/dist/run/bundle.d.ts +11 -1
  31. package/dist/run/judge.d.ts +7 -0
  32. package/dist/run/judge.js +4 -4
  33. package/dist/run/notes.js +1 -1
  34. package/dist/run/outcomes.d.ts +8 -2
  35. package/dist/run/outcomes.js +56 -14
  36. package/dist/study/arrivals.d.ts +84 -31
  37. package/dist/study/arrivals.js +165 -86
  38. package/dist/study/bundle-estimate.d.ts +8 -7
  39. package/dist/study/bundle-estimate.js +131 -70
  40. package/dist/study/desktop-files.js +4 -4
  41. package/dist/study/plan-participants.d.ts +4 -9
  42. package/dist/study/plan-participants.js +2 -14
  43. package/dist/study/plan-types.d.ts +6 -1
  44. package/dist/study/plan.d.ts +2 -1
  45. package/dist/study/plan.js +6 -4
  46. package/dist/study/types.d.ts +2 -1
  47. package/dist/substrates/e2b/lifetime.d.ts +22 -8
  48. package/dist/substrates/e2b/lifetime.js +54 -10
  49. package/docs/contracts/run-bundle.md +1 -1
  50. package/docs/contracts/schemas.md +24 -11
  51. package/package.json +1 -1
@@ -1,9 +1,11 @@
1
1
  // When each participant of a computer-use or shared-world run starts, and on computer use when it
2
2
  // ends. A study may give each participant an offset from the moment the run starts its participants
3
3
  // (`participants[].startAfterMs`) and, on computer use, an end on the same clock
4
- // (`participants[].endAfterMs`), with a wind-down before it in which each turn tells the
5
- // participant the time left (`windDownMs`). The plan simulates the start queue to report the schedule and
6
- // when the last session ends at the latest; the routes start their participants through
4
+ // (`participants[].endAfterMs`) or a cap on its own session (`maxSessionMs`), with a wind-down
5
+ // before either in which each turn tells the participant the time left (`windDownMs`). Each
6
+ // participant's ParticipantSchedule resolves those fields once; everything that reads a
7
+ // participant's time reads the schedule. The plan simulates the start queue to report the schedule
8
+ // and when the last session ends at the latest; the routes start their participants through
7
9
  // runOnSchedule, which waits for each one's time and then for a free slot. A participant's desktop
8
10
  // is created when it starts, so a late start holds no sandbox before then.
9
11
  import { setTimeout as delay } from "node:timers/promises";
@@ -22,8 +24,8 @@ export function isStartAfterMs(value) {
22
24
  value >= 0 &&
23
25
  value <= LATEST_START_AFTER_MS);
24
26
  }
25
- /** How long before its end a participant is told the time left, unless it declares `windDownMs`. */
26
- export const DEFAULT_WIND_DOWN_MS = 10 * 60_000;
27
+ /** The longest default wind-down: 10 minutes is enough to wrap up what a participant is doing. */
28
+ const MAX_DEFAULT_WIND_DOWN_MS = 10 * 60_000;
27
29
  /**
28
30
  * Room a scheduled participant's session keeps past its end for the turn in flight and the closing
29
31
  * report. The loop ends the session at the end; this keeps the session's own time limit, and the
@@ -58,79 +60,120 @@ export function windowReason(entry, path) {
58
60
  return undefined;
59
61
  }
60
62
  /**
61
- * A participant's session budget: with a declared end, its window plus SCHEDULED_END_ROOM_MS, and
62
- * with a cap, the cap plus the same room, in place of the study's `sessionMs` (execution.timeoutMs
63
- * or its default); else `sessionMs`.
63
+ * The schedule of a participants entry that windowReason accepts. A cap wins over an end, which
64
+ * windowReason refuses beside it anyway.
64
65
  */
65
- export function participantSessionMs(participant, sessionMs) {
66
- const windowMs = declaredWindowMs(participant);
67
- return windowMs === undefined ? sessionMs : windowMs + SCHEDULED_END_ROOM_MS;
66
+ export function participantSchedule(entry) {
67
+ const start = entry.startAfterMs === undefined ? {} : { startAfterMs: entry.startAfterMs };
68
+ if (entry.maxSessionMs !== undefined)
69
+ return {
70
+ ...start,
71
+ end: { kind: "session_cap", ...endTiming(entry.maxSessionMs, entry.windDownMs) },
72
+ };
73
+ if (entry.endAfterMs === undefined)
74
+ return start;
75
+ const windowMs = entry.endAfterMs - (entry.startAfterMs ?? 0);
76
+ return {
77
+ ...start,
78
+ end: {
79
+ kind: "scheduled_end",
80
+ endAfterMs: entry.endAfterMs,
81
+ ...endTiming(windowMs, entry.windDownMs),
82
+ },
83
+ };
68
84
  }
69
- /** How long a participant with a declared end or cap may work, or undefined without either. */
70
- function declaredWindowMs(participant) {
71
- if (participant.maxSessionMs !== undefined)
72
- return participant.maxSessionMs;
73
- if (participant.endAfterMs === undefined)
74
- return undefined;
75
- return participant.endAfterMs - (participant.startAfterMs ?? 0);
85
+ /**
86
+ * A window's wind-down and session budget. Without a declared `windDownMs` the wind-down is a
87
+ * quarter of the window, at most 10 minutes. A fixed 10 minutes covered the whole of a window of
88
+ * 10 minutes or less, so a short session was told its end from the first turn. A quarter leaves
89
+ * most of a short session without a countdown, and a window of 40 minutes or more keeps the 10
90
+ * minutes.
91
+ */
92
+ function endTiming(windowMs, windDownMs) {
93
+ return {
94
+ windowMs,
95
+ windDownMs: windDownMs ?? Math.min(MAX_DEFAULT_WIND_DOWN_MS, Math.floor(windowMs / 4)),
96
+ sessionMs: windowMs + SCHEDULED_END_ROOM_MS,
97
+ };
98
+ }
99
+ /**
100
+ * The schedule a rerun gives a participant: it starts with the rerun, and a scheduled end keeps its
101
+ * time on the rerun's clock and its wind-down, so its window runs from the rerun's start.
102
+ */
103
+ export function startingWithTheRun(schedule) {
104
+ const { end } = schedule;
105
+ if (end === undefined)
106
+ return {};
107
+ if (end.kind === "session_cap")
108
+ return { end };
109
+ return participantSchedule({ endAfterMs: end.endAfterMs, windDownMs: end.windDownMs });
110
+ }
111
+ /**
112
+ * A participant's own session budget and sandbox deadline. With an end or a cap it is the end's
113
+ * `sessionMs`, and the sandbox keeps the study sandbox's headroom over the study session on top;
114
+ * without one, the study's own.
115
+ */
116
+ export function participantSession(schedule, study) {
117
+ const sessionMs = schedule.end?.sessionMs ?? study.sessionMs;
118
+ return { sessionMs, sandboxMs: study.sandboxMs - study.sessionMs + sessionMs };
76
119
  }
77
120
  /**
78
121
  * How a participant that was due at `scheduledAt` (runOnSchedule's ScheduledStart) ends, or
79
- * undefined when it declares neither an end nor a cap. A participant that waited for a slot still
80
- * ends at its declared time, and still gets its whole cap.
122
+ * undefined without an end or a cap. A scheduled end comes its window after the participant's
123
+ * time, which is its `endAfterMs` on the run's clock, even when it waited for a slot; a cap is
124
+ * whole from wherever its session starts.
81
125
  */
82
- export function participantEnd(participant, scheduledAt) {
83
- const windDownMs = participant.windDownMs ?? DEFAULT_WIND_DOWN_MS;
84
- if (participant.maxSessionMs !== undefined)
85
- return { kind: "session_cap", afterMs: participant.maxSessionMs, windDownMs };
86
- if (participant.endAfterMs === undefined)
126
+ export function participantEnd(schedule, scheduledAt) {
127
+ const { end } = schedule;
128
+ if (end === undefined)
87
129
  return undefined;
88
- return {
89
- kind: "scheduled_end",
90
- atMs: scheduledAt - (participant.startAfterMs ?? 0) + participant.endAfterMs,
91
- windDownMs,
92
- };
130
+ if (end.kind === "session_cap")
131
+ return { kind: "session_cap", afterMs: end.windowMs, windDownMs: end.windDownMs };
132
+ return { kind: "scheduled_end", atMs: scheduledAt + end.windowMs, windDownMs: end.windDownMs };
93
133
  }
94
134
  /**
95
135
  * Why the sandbox of the first participant whose window or cap passes `ceilingMs` cannot live that
96
- * long, or undefined. Its deadline is its session (participantSessionMs) plus `headroomMs`, what the
97
- * route adds to a session for setup and teardown (sandboxHeadroomMs).
136
+ * long, or undefined. Its deadline is its participantSession's, from the study's session budget and
137
+ * sandbox deadline.
98
138
  */
99
- export function windowDeadlineRefusal(participants, sandbox, ceilingMs) {
139
+ export function windowDeadlineRefusal(participants, study, ceilingMs) {
100
140
  const inMinutes = (ms) => Math.round(ms / 60_000);
101
- for (const participant of participants) {
102
- if (declaredWindowMs(participant) === undefined)
141
+ for (const { id, schedule } of participants) {
142
+ const { end } = schedule;
143
+ if (end === undefined)
103
144
  continue;
104
- const deadlineMs = participantSessionMs(participant, sandbox.sessionMs) + sandbox.headroomMs;
145
+ const deadlineMs = participantSession(schedule, study).sandboxMs;
105
146
  if (deadlineMs <= ceilingMs)
106
147
  continue;
107
- const name = participant.id === undefined ? "A participant" : `Participant "${participant.id}"`;
108
- const works = participant.maxSessionMs === undefined
109
- ? `works from +${formatDuration(participant.startAfterMs ?? 0)} to +${formatDuration(participant.endAfterMs ?? 0)}`
110
- : `may work for up to ${formatDuration(participant.maxSessionMs)}`;
111
- const window = participant.maxSessionMs === undefined ? "window" : "cap";
112
- return `${name} ${works}, so its sandbox needs a ${inMinutes(deadlineMs)}m deadline: the ${window}, ${inMinutes(SCHEDULED_END_ROOM_MS)}m for its closing report and ${inMinutes(sandbox.headroomMs)}m to set up and tear down. A sandbox may not live longer than ${ceilingMs / 60_000}m. Shorten its ${window}.${ceilingAdvice(deadlineMs)}`;
148
+ const works = end.kind === "scheduled_end"
149
+ ? `works from +${formatDuration(schedule.startAfterMs ?? 0)} to +${formatDuration(end.endAfterMs)}`
150
+ : `may work for up to ${formatDuration(end.windowMs)}`;
151
+ const window = end.kind === "scheduled_end" ? "window" : "cap";
152
+ return `Participant "${id}" ${works}, so its sandbox needs a ${inMinutes(deadlineMs)}m deadline: the ${window}, ${inMinutes(SCHEDULED_END_ROOM_MS)}m for its closing report and ${inMinutes(study.sandboxMs - study.sessionMs)}m to set up and tear down. A sandbox may not live longer than ${ceilingMs / 60_000}m. Shorten its ${window}.${ceilingAdvice(deadlineMs)}`;
113
153
  }
114
154
  return undefined;
115
155
  }
116
156
  /**
117
157
  * Each participant's window for `study check`, when any participant declares an end or a cap: when
118
158
  * it starts and ends and when it is first told the time left, when it starts and its cap and how
119
- * far into its session it is first told the time left, or when it starts and its session budget.
159
+ * far into its session it is first told the time left, or when it starts and the study's
160
+ * `sessionMs`.
120
161
  */
121
162
  export function describeWindows(participants, sessionMs) {
122
- if (!participants.some((participant) => declaredWindowMs(participant) !== undefined))
163
+ if (participants.every((participant) => participant.schedule.end === undefined))
123
164
  return undefined;
124
- const windows = participants.map((participant) => {
125
- const start = `${participant.id ?? "participant"} from +${formatDuration(participant.startAfterMs ?? 0)}`;
126
- if (participant.maxSessionMs !== undefined) {
127
- const told = Math.max(0, participant.maxSessionMs - (participant.windDownMs ?? DEFAULT_WIND_DOWN_MS));
128
- return `${start} until it ends itself or for up to ${formatDuration(participant.maxSessionMs)}, told the time left from ${formatDuration(told)} into its session`;
129
- }
130
- if (participant.endAfterMs === undefined)
165
+ const windows = participants.map(({ id, schedule }) => {
166
+ const startAfterMs = schedule.startAfterMs ?? 0;
167
+ const start = `${id} from +${formatDuration(startAfterMs)}`;
168
+ const { end } = schedule;
169
+ if (end === undefined)
131
170
  return `${start} for up to ${formatDuration(sessionMs)}`;
132
- const told = Math.max(participant.startAfterMs ?? 0, participant.endAfterMs - (participant.windDownMs ?? DEFAULT_WIND_DOWN_MS));
133
- return `${start} to +${formatDuration(participant.endAfterMs)}, told the time left from +${formatDuration(told)}`;
171
+ if (end.kind === "session_cap") {
172
+ const told = Math.max(0, end.windowMs - end.windDownMs);
173
+ return `${start} until it ends itself or for up to ${formatDuration(end.windowMs)}, told the time left from ${formatDuration(told)} into its session`;
174
+ }
175
+ const told = Math.max(startAfterMs, end.endAfterMs - end.windDownMs);
176
+ return `${start} to +${formatDuration(end.endAfterMs)}, told the time left from +${formatDuration(told)}`;
134
177
  });
135
178
  return `${windows.join("; ")}.`;
136
179
  }
@@ -152,34 +195,42 @@ export function rosterStartsReason(roster) {
152
195
  return undefined;
153
196
  }
154
197
  /**
155
- * The schedule a roster's declared offsets give under the queue (`undefined` is 0: the participant
156
- * starts with the run). Participants start in schedule order, by offset and then roster position,
157
- * each at its time or when a slot frees, whichever is later; this is the rule runOnSchedule
158
- * follows. `sessionsMs` gives a participant with a declared end its own session budget
159
- * (participantSessionMs); the others hold their slot for `queue.sessionMs`.
198
+ * The schedule a roster's participants give under the queue. Participants start in schedule order,
199
+ * by offset and then roster position, each at its time or when a slot frees, whichever is later;
200
+ * this is the rule runOnSchedule follows. A participant with an end or a cap holds its slot for its
201
+ * own session budget, and the others for `queue.sessionMs`. A participant whose slot would free
202
+ * only at or after its scheduled end takes none: the run skips it (pastEndReason).
160
203
  */
161
- export function planArrivals(offsets, queue, sessionsMs = []) {
162
- const due = offsets.map((offset) => offset ?? 0);
163
- const session = (index) => sessionsMs[index] ?? queue.sessionMs;
164
- const starts = queueStarts(due, queue, session);
204
+ export function planArrivals(schedules, queue) {
205
+ return simulateQueue(schedules, queue).arrivals;
206
+ }
207
+ /** planArrivals, and the roster indexes of the participants the run would skip. */
208
+ function simulateQueue(schedules, queue) {
209
+ const due = schedules.map((schedule) => schedule.startAfterMs ?? 0);
210
+ const sessionsMs = schedules.map((schedule) => schedule.end?.sessionMs ?? queue.sessionMs);
211
+ const session = (index) => sessionsMs[index];
212
+ const { starts, skipped } = queueStarts(schedules, due, queue, session);
213
+ const skips = new Set(skipped);
214
+ const started = starts.flatMap((_, index) => (skips.has(index) ? [] : [index]));
165
215
  let waiting = 0;
166
216
  let longestWaitMs = 0;
167
- for (const [index, start] of starts.entries()) {
168
- const waitMs = start - due[index];
217
+ for (const index of started) {
218
+ const waitMs = starts[index] - due[index];
169
219
  if (waitMs > 0)
170
220
  waiting += 1;
171
221
  longestWaitMs = Math.max(longestWaitMs, waitMs);
172
222
  }
173
- return {
174
- declared: offsets.some((offset) => offset !== undefined),
223
+ const arrivals = {
224
+ declared: schedules.some((schedule) => schedule.startAfterMs !== undefined),
175
225
  firstStartMs: Math.min(...due),
176
226
  lastStartMs: Math.max(...due),
177
- peak: mostAtOnce(starts, session),
227
+ peak: mostAtOnce(started.map((index) => [starts[index], session(index)])),
178
228
  waiting,
179
229
  longestWaitMs,
180
- lastEndMs: Math.max(...starts.map((start, index) => start + session(index))),
230
+ lastEndMs: Math.max(...started.map((index) => starts[index] + session(index))),
181
231
  sessionMs: queue.sessionMs,
182
232
  };
233
+ return { arrivals, skipped };
183
234
  }
184
235
  /**
185
236
  * The plan's schedule, after a "schedule:" label: the first and last start, and how many run at
@@ -195,21 +246,43 @@ export function describeArrivals(arrivals) {
195
246
  * A planner's arrival plan for its participants under the start queue, and the warnings the plan
196
247
  * records about it. A participant's desktop is created when it starts, so on computer use a late
197
248
  * start adds nothing to any sandbox's deadline; the provisioned shared world checks its app's
198
- * sandbox with servedScheduleRefusal. `lowered` is the warning that the E2B plan's limit runs the
199
- * roster in waves.
249
+ * sandbox with servedScheduleRefusal. `lowered` is participantsAtOnce's warning that the E2B plan's
250
+ * limit or HUMANISH_CUA_MAX_CONCURRENCY runs the roster in waves.
200
251
  */
201
252
  export function planStarts(participants, queue, lowered) {
202
- const arrivals = planArrivals(participants.map((participant) => participant.startAfterMs), queue, participants.map((participant) => participantSessionMs(participant, queue.sessionMs)));
203
- return { arrivals, warnings: startQueueWarnings(arrivals, queue, lowered) };
253
+ const { arrivals, skipped } = simulateQueue(participants.map((participant) => participant.schedule), queue);
254
+ const ids = skipped.map((index) => participants[index].id);
255
+ return { arrivals, warnings: startQueueWarnings(arrivals, ids, queue, lowered) };
204
256
  }
205
257
  // A roster that starts together keeps `lowered`. A declared schedule spreads its participants
206
- // itself, so it is warned only when it holds someone past their time, with `lowered` beside that
207
- // when the limit is the reason.
208
- function startQueueWarnings(arrivals, queue, lowered) {
209
- if (!arrivals.declared || arrivals.waiting === 0)
210
- return !arrivals.declared && lowered ? [lowered] : [];
211
- const late = `${plural(arrivals.waiting, "participant")} would start after their time when every session uses its ${formatDuration(arrivals.sessionMs)} budget, the latest by ${formatDuration(arrivals.longestWaitMs)}: the run starts at most ${queue.slots} at once and holds the next one until a slot frees.`;
212
- return lowered === undefined ? [late] : [late, lowered];
258
+ // itself, so it is warned only when it holds someone past their time or skips someone, with
259
+ // `lowered` beside that when the limit is the reason.
260
+ function startQueueWarnings(arrivals, skipped, queue, lowered) {
261
+ const budget = `when every session uses its ${formatDuration(arrivals.sessionMs)} budget`;
262
+ const atMost = `the run starts at most ${queue.slots} at once`;
263
+ const late = `${plural(arrivals.waiting, "participant")} would start after their time ${budget}, the latest by ${formatDuration(arrivals.longestWaitMs)}: ${atMost} and holds the next one until a slot frees.`;
264
+ const one = skipped.length === 1;
265
+ const names = skipped.map((id) => `"${id}"`);
266
+ const who = one
267
+ ? `Participant ${names[0]}`
268
+ : `${skipped.length} participants (${names.join(", ")})`;
269
+ const skip = `${who} would still be waiting for a free slot at ${one ? "its" : "their"} end (participants[].endAfterMs) ${budget}, so the run would skip ${one ? "it without creating its desktop" : "them without creating their desktops"}: ${atMost}. Let more participants run at once, or give ${one ? "it" : "them"} a later end.`;
270
+ const held = arrivals.waiting > 0 || skipped.length > 0;
271
+ return [
272
+ ...(arrivals.declared && arrivals.waiting > 0 ? [late] : []),
273
+ ...(skipped.length > 0 ? [skip] : []),
274
+ ...(lowered !== undefined && (!arrivals.declared || held) ? [lowered] : []),
275
+ ];
276
+ }
277
+ /**
278
+ * Why a participant whose start comes at `nowMs` is skipped, or undefined: its scheduled end came
279
+ * while it waited, so its session would end before it began. The plan's simulation applies the
280
+ * same rule (planArrivals).
281
+ */
282
+ export function pastEndReason(end, nowMs) {
283
+ if (end?.kind !== "scheduled_end" || nowMs < end.atMs)
284
+ return undefined;
285
+ return "skipped: its scheduled end (participants[].endAfterMs) came while it waited to start, so its session would have ended before it began; no desktop was created";
213
286
  }
214
287
  /**
215
288
  * Why a sandbox that serves the app until the last participant ends, the provisioned shared
@@ -243,8 +316,9 @@ function scheduleOrder(due) {
243
316
  return due.map((_, index) => index).sort((a, b) => due[a] - due[b] || a - b);
244
317
  }
245
318
  /** Each participant's start in roster order, when each session holds its slot for `session(index)`. */
246
- function queueStarts(due, queue, session) {
319
+ function queueStarts(schedules, due, queue, session) {
247
320
  const starts = [...due];
321
+ const skipped = [];
248
322
  const queued = scheduleOrder(due).filter((index) => index !== queue.ownSlot);
249
323
  const pooled = queue.ownSlot === undefined ? queue.slots : queue.slots - 1;
250
324
  // When each slot is next free; a participant takes the one that frees first.
@@ -253,16 +327,21 @@ function queueStarts(due, queue, session) {
253
327
  const slot = free.indexOf(Math.min(...free));
254
328
  const start = Math.max(due[index], free[slot]);
255
329
  starts[index] = start;
256
- free[slot] = start + session(index);
330
+ const end = schedules[index]?.end;
331
+ if (end?.kind === "scheduled_end" && start >= end.endAfterMs)
332
+ skipped.push(index);
333
+ else
334
+ free[slot] = start + session(index);
257
335
  }
258
- return starts;
336
+ return { starts, skipped };
259
337
  }
260
- function mostAtOnce(starts, session) {
338
+ /** The most sessions running at once, each given as its start and how long it holds its slot. */
339
+ function mostAtOnce(sessions) {
261
340
  // An end sorts before a start at the same instant: that slot is free again.
262
- const changes = starts
263
- .flatMap((start, index) => [
341
+ const changes = sessions
342
+ .flatMap(([start, ms]) => [
264
343
  { at: start, by: 1 },
265
- { at: start + session(index), by: -1 },
344
+ { at: start + ms, by: -1 },
266
345
  ])
267
346
  .sort((a, b) => a.at - b.at || a.by - b.by);
268
347
  let live = 0;
@@ -1,9 +1,8 @@
1
1
  import type { DwellWindow } from "../actors/stop-conditions.js";
2
- /** One participant as the plan has it: when it starts and ends, its cap, and its dwell window. */
2
+ import type { ParticipantSchedule } from "./arrivals.js";
3
+ /** One participant as the plan has it: its schedule and its dwell window. */
3
4
  export interface EstimatedParticipant {
4
- readonly startAfterMs?: number;
5
- readonly endAfterMs?: number;
6
- readonly maxSessionMs?: number;
5
+ readonly schedule: ParticipantSchedule;
7
6
  readonly limits: {
8
7
  readonly dwell?: DwellWindow;
9
8
  };
@@ -12,8 +11,10 @@ export interface EstimatedParticipant {
12
11
  export interface BundleEstimate {
13
12
  /** run.json, which analysis reads whole. */
14
13
  readonly runFileBytes: number;
15
- /** The largest file: observer/observer-data.json, or the Observer page for a small run. */
16
- readonly largestFileBytes: number;
14
+ /** The largest participant trace file, which analysis also reads whole. */
15
+ readonly largestTraceBytes: number;
16
+ /** observer/index.html, which verify reads whole. */
17
+ readonly observerPageBytes: number;
17
18
  /** Every file, screenshots included. */
18
19
  readonly totalBytes: number;
19
20
  /** Files and folders in the run folder, as the public-safety scan counts them. */
@@ -42,7 +43,7 @@ export interface BundleEstimate {
42
43
  export declare function estimateBundle(participants: readonly EstimatedParticipant[], sessionMs: number): BundleEstimate;
43
44
  /**
44
45
  * The warning for a run whose estimate passes a limit, or undefined: each limit it passes, what the
45
- * estimate counted, how high it runs, and what to change.
46
+ * estimate counted, and what to change.
46
47
  */
47
48
  export declare function bundleLimitReason(estimate: BundleEstimate): string | undefined;
48
49
  /** The estimate beside the limits, for `study check`. */