humanish 0.125.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 (128) hide show
  1. package/CHANGELOG.md +29 -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/clock.d.ts +9 -8
  5. package/dist/actors/computer-use/loop/clock.js +27 -17
  6. package/dist/actors/computer-use/loop/debrief.d.ts +1 -1
  7. package/dist/actors/computer-use/loop/debrief.js +15 -5
  8. package/dist/actors/computer-use/loop/ending.d.ts +10 -5
  9. package/dist/actors/computer-use/loop/ending.js +12 -7
  10. package/dist/actors/computer-use/loop/observation.d.ts +6 -0
  11. package/dist/actors/computer-use/loop/observation.js +19 -1
  12. package/dist/actors/computer-use/loop/session.d.ts +4 -2
  13. package/dist/actors/computer-use/loop/session.js +4 -1
  14. package/dist/actors/computer-use/loop/trace.js +4 -2
  15. package/dist/actors/computer-use/loop/types.d.ts +22 -8
  16. package/dist/actors/computer-use/loop.js +5 -5
  17. package/dist/actors/computer-use/openai-provider.js +1 -1
  18. package/dist/actors/contract.d.ts +31 -8
  19. package/dist/actors/contract.js +3 -0
  20. package/dist/actors/goal-source.d.ts +32 -2
  21. package/dist/actors/goal-source.js +41 -2
  22. package/dist/actors/stop-cause.js +1 -0
  23. package/dist/analysis/evidence-sources.js +2 -2
  24. package/dist/analysis/types.d.ts +2 -2
  25. package/dist/analysis/validation.js +2 -1
  26. package/dist/cli/commands/study.js +8 -0
  27. package/dist/evidence/redaction.d.ts +1 -0
  28. package/dist/evidence/redaction.js +1 -0
  29. package/dist/evidence/trace-page.d.ts +6 -0
  30. package/dist/evidence/trace-page.js +90 -0
  31. package/dist/observer/data.js +8 -5
  32. package/dist/observer/feed-cursors.d.ts +14 -0
  33. package/dist/observer/feed-cursors.js +61 -0
  34. package/dist/observer/participant-items.d.ts +7 -0
  35. package/dist/observer/pinned-files.d.ts +2 -1
  36. package/dist/observer/pinned-files.js +2 -2
  37. package/dist/observer/run-routes.d.ts +1 -1
  38. package/dist/observer/run-routes.js +38 -17
  39. package/dist/observer-app.html +7 -7
  40. package/dist/routes/computer-use/bundle.js +4 -0
  41. package/dist/routes/computer-use/diagnostics.d.ts +1 -1
  42. package/dist/routes/computer-use/diagnostics.js +3 -3
  43. package/dist/routes/computer-use/e2b-desktop/desktop.js +2 -1
  44. package/dist/routes/computer-use/e2b-desktop/prepare.d.ts +6 -0
  45. package/dist/routes/computer-use/e2b-desktop/prepare.js +47 -1
  46. package/dist/routes/computer-use/e2b-desktop/start.d.ts +2 -1
  47. package/dist/routes/computer-use/e2b-desktop/start.js +15 -3
  48. package/dist/routes/computer-use/e2b-desktop/state.d.ts +3 -1
  49. package/dist/routes/computer-use/e2b-desktop/state.js +2 -0
  50. package/dist/routes/computer-use/fanout-bundle.js +3 -0
  51. package/dist/routes/computer-use/fanout-records.js +5 -1
  52. package/dist/routes/computer-use/participant-deps.d.ts +1 -1
  53. package/dist/routes/computer-use/participant-desktop.d.ts +1 -1
  54. package/dist/routes/computer-use/participant-execution.js +6 -2
  55. package/dist/routes/computer-use/participant-facts.js +4 -1
  56. package/dist/routes/computer-use/participant-model.js +16 -6
  57. package/dist/routes/computer-use/participant-runs.d.ts +10 -10
  58. package/dist/routes/computer-use/participant-runs.js +54 -84
  59. package/dist/routes/computer-use/plan.d.ts +2 -0
  60. package/dist/routes/computer-use/plan.js +65 -17
  61. package/dist/routes/computer-use/rerun-selection.js +8 -5
  62. package/dist/routes/computer-use/setup.d.ts +1 -0
  63. package/dist/routes/computer-use/setup.js +29 -1
  64. package/dist/routes/computer-use/single-bundle.d.ts +5 -1
  65. package/dist/routes/computer-use/single-bundle.js +11 -1
  66. package/dist/routes/computer-use/types.d.ts +18 -13
  67. package/dist/routes/computer-use/types.js +0 -3
  68. package/dist/routes/shared-world/bundle-records.js +4 -1
  69. package/dist/routes/shared-world/external-public.js +1 -1
  70. package/dist/routes/shared-world/participant-specs.js +3 -2
  71. package/dist/routes/shared-world/plan.d.ts +2 -0
  72. package/dist/routes/shared-world/plan.js +8 -3
  73. package/dist/routes/shared-world/provisioned.js +1 -1
  74. package/dist/run/bundle.d.ts +11 -1
  75. package/dist/run/contained-output.d.ts +7 -3
  76. package/dist/run/contained-output.js +11 -7
  77. package/dist/run/judge.d.ts +14 -4
  78. package/dist/run/judge.js +17 -12
  79. package/dist/run/live-items.d.ts +19 -0
  80. package/dist/run/live-items.js +28 -8
  81. package/dist/run/notes.js +1 -1
  82. package/dist/run/outcomes.d.ts +8 -2
  83. package/dist/run/outcomes.js +56 -14
  84. package/dist/run/stream-trace.d.ts +25 -4
  85. package/dist/run/stream-trace.js +55 -17
  86. package/dist/run/streams.d.ts +16 -0
  87. package/dist/study/arrivals.d.ts +105 -34
  88. package/dist/study/arrivals.js +179 -74
  89. package/dist/study/bundle-estimate.d.ts +12 -10
  90. package/dist/study/bundle-estimate.js +138 -72
  91. package/dist/study/desktop-files.d.ts +37 -0
  92. package/dist/study/desktop-files.js +174 -0
  93. package/dist/study/keys.d.ts +8 -0
  94. package/dist/study/keys.js +3 -0
  95. package/dist/study/migrate/v2.js +12 -3
  96. package/dist/study/parse/actors.d.ts +1 -1
  97. package/dist/study/parse/actors.js +28 -20
  98. package/dist/study/parse/clock.d.ts +18 -0
  99. package/dist/study/parse/clock.js +36 -0
  100. package/dist/study/plan-participants.d.ts +19 -10
  101. package/dist/study/plan-participants.js +13 -9
  102. package/dist/study/plan-types.d.ts +6 -1
  103. package/dist/study/plan.d.ts +2 -1
  104. package/dist/study/plan.js +28 -15
  105. package/dist/study/preflight.js +4 -0
  106. package/dist/study/types.d.ts +35 -5
  107. package/dist/study/validation.js +3 -0
  108. package/dist/study/warnings.d.ts +2 -2
  109. package/dist/study/warnings.js +7 -0
  110. package/dist/subject/local-tree-archive.d.ts +7 -0
  111. package/dist/subject/local-tree-archive.js +12 -0
  112. package/dist/substrates/e2b/desktop-browser.d.ts +4 -1
  113. package/dist/substrates/e2b/desktop-browser.js +6 -2
  114. package/dist/substrates/e2b/desktop-clock.d.ts +10 -0
  115. package/dist/substrates/e2b/desktop-clock.js +29 -0
  116. package/dist/substrates/e2b/lifetime.d.ts +22 -8
  117. package/dist/substrates/e2b/lifetime.js +54 -10
  118. package/dist/substrates/e2b/sdk.d.ts +6 -0
  119. package/dist/verify/pages.d.ts +7 -0
  120. package/dist/verify/pages.js +50 -0
  121. package/dist/verify/verify.js +10 -3
  122. package/docs/architecture/actor-contract.md +2 -0
  123. package/docs/architecture/external-public-shared-world.md +4 -1
  124. package/docs/architecture/observer.md +10 -1
  125. package/docs/contracts/policy.md +7 -0
  126. package/docs/contracts/run-bundle.md +31 -1
  127. package/docs/contracts/schemas.md +93 -11
  128. package/package.json +2 -2
@@ -8,7 +8,7 @@ import { createHash } from "node:crypto";
8
8
  import { RUN_BUNDLE_FILE, RUN_BUNDLE_SCHEMA, RUN_BUNDLE_V2_SCHEMA, } from "./bundle.js";
9
9
  import { isRunBundle } from "./bundle-shape.js";
10
10
  import { readContainedPrefix, readContainedRegularFile, refusalText, RUN_ARTIFACT_MAX_BYTES, } from "./contained-output.js";
11
- import { isLiveItemsIndex, LIVE_ACTOR_SCHEMA, LIVE_ACTOR_V1_SCHEMA, liveItemsFromBytes, } from "./live-items.js";
11
+ import { isLiveItemsIndex, LIVE_ACTOR_SCHEMA, LIVE_ACTOR_V1_SCHEMA, liveItemsAfter, liveItemsFromBytes, } from "./live-items.js";
12
12
  import { isRunStreamArtifact, isStreamTraceIndex, isStreamTracePath } from "./stream-shape.js";
13
13
  import { isRecord } from "./type-guards.js";
14
14
  /** A trace that does not match its index, or cannot be read. The message names the stream. */
@@ -87,10 +87,10 @@ export function splitStreamTraces(bundle) {
87
87
  return { bundle: { ...bundle, schema: RUN_BUNDLE_SCHEMA, streams }, files };
88
88
  }
89
89
  function containedReader(runPaths) {
90
- return async (relativePath, maxBytes, length) => {
90
+ return async (relativePath, maxBytes, length, start) => {
91
91
  const read = length === undefined
92
92
  ? await readContainedRegularFile(runPaths, relativePath, maxBytes)
93
- : await readContainedPrefix(runPaths, relativePath, length);
93
+ : await readContainedPrefix(runPaths, relativePath, length, start);
94
94
  if (read.status === "missing")
95
95
  return { refused: `${relativePath} is missing` };
96
96
  if (read.status === "refused")
@@ -152,7 +152,7 @@ class LiveFileUnreadableError extends StreamTraceRefusedError {
152
152
  * file's path and those bytes. A live actor with inline items (humanish.live-actor.v1) is returned
153
153
  * as it is; bundleRefusal refuses one in a v3 run.json.
154
154
  */
155
- async function readLiveItems(read, stream, maxBytes) {
155
+ async function readLiveItems(read, stream, maxBytes, liveAfter) {
156
156
  const live = stream.liveActor;
157
157
  if (live === undefined || isInlineLiveActor(live))
158
158
  return { stream };
@@ -168,6 +168,15 @@ async function readLiveItems(read, stream, maxBytes) {
168
168
  refuse("run.json holds in-progress items beside their live file");
169
169
  if (index.bytes > maxBytes)
170
170
  refuse(`${index.path} is indexed at ${index.bytes} bytes, more than the ${maxBytes} read`);
171
+ const { itemsFile: _index, ...summary } = live;
172
+ const after = liveAfter?.(index);
173
+ const tail = after === undefined ? undefined : await readLiveTail(read, stream, index, after);
174
+ if (tail !== undefined)
175
+ return {
176
+ stream: { ...stream, liveActor: { ...summary, items: tail.items } },
177
+ file: { path: index.path, bytes: tail.bytes },
178
+ read: { streamId: stream.id, index, after: after },
179
+ };
171
180
  const file = await read(index.path, maxBytes, index.bytes);
172
181
  if ("refused" in file)
173
182
  throw new LiveFileUnreadableError(stream.id, file.refused);
@@ -175,17 +184,38 @@ async function readLiveItems(read, stream, maxBytes) {
175
184
  const joined = liveItemsFromBytes(file.bytes.subarray(0, index.bytes), index);
176
185
  if ("refused" in joined)
177
186
  return refuse(joined.refused);
178
- const { itemsFile: _index, ...summary } = live;
179
187
  return {
180
188
  stream: { ...stream, liveActor: { ...summary, items: joined.items } },
181
- live: { path: index.path, bytes: file.bytes.subarray(0, index.bytes) },
189
+ file: { path: index.path, bytes: file.bytes.subarray(0, index.bytes) },
190
+ read: { streamId: stream.id, index },
182
191
  };
183
192
  }
193
+ /**
194
+ * The items of the live file `index` describes past `after`, and the bytes they were read from;
195
+ * undefined when the cursor does not fit the index or the file's lines, so the caller reads the
196
+ * whole prefix. A cursor at the indexed end reads nothing. A file the reader cannot read throws
197
+ * LiveFileUnreadableError, as a whole read does.
198
+ */
199
+ async function readLiveTail(read, stream, index, after) {
200
+ if (after.bytes === 0 ||
201
+ after.items > index.items ||
202
+ after.bytes > index.bytes ||
203
+ (after.items === index.items) !== (after.bytes === index.bytes))
204
+ return undefined;
205
+ if (after.bytes === index.bytes)
206
+ return { items: [], bytes: Buffer.alloc(0) };
207
+ const file = await read(index.path, index.bytes - after.bytes + 1, index.bytes, after.bytes - 1);
208
+ if ("refused" in file)
209
+ throw new LiveFileUnreadableError(stream.id, file.refused);
210
+ const tail = file.bytes.subarray(0, index.bytes - after.bytes + 1);
211
+ const joined = liveItemsAfter(tail, index, after);
212
+ return "refused" in joined ? undefined : { items: joined.items, bytes: tail.subarray(1) };
213
+ }
184
214
  /**
185
215
  * The stream with its trace and live items joined, and each file's path and the bytes it was
186
216
  * joined from.
187
217
  */
188
- async function readStreamTrace(read, stream, maxBytes) {
218
+ async function readStreamTrace(read, stream, maxBytes, liveAfter) {
189
219
  const files = [];
190
220
  let joined = stream;
191
221
  if (stream.trace !== undefined) {
@@ -196,10 +226,10 @@ async function readStreamTrace(read, stream, maxBytes) {
196
226
  joined = joinTrace(stream, file.bytes);
197
227
  files.push({ path: stream.trace.path, bytes: file.bytes });
198
228
  }
199
- const live = await readLiveItems(read, joined, maxBytes);
200
- if (live.live !== undefined)
201
- files.push(live.live);
202
- return { stream: live.stream, files };
229
+ const live = await readLiveItems(read, joined, maxBytes, liveAfter);
230
+ if (live.file !== undefined)
231
+ files.push(live.file);
232
+ return { stream: live.stream, files, ...(live.read === undefined ? {} : { live: live.read }) };
203
233
  }
204
234
  /**
205
235
  * The stream as a v1 run.json held it: its actor with every item and every artifact entry. A
@@ -249,6 +279,11 @@ function bundleRefusal(bundle, stream, owners) {
249
279
  * receives each trace and live file's path and the bytes the stream was joined from, after they
250
280
  * matched the index, so a caller scans exactly what it reads.
251
281
  *
282
+ * `liveAfter` names, for a live file's index, the prefix the caller holds already (the Observer
283
+ * feed's cursor). The loader then reads and joins only the items past it, checked by liveItemsAfter
284
+ * without the indexed sha256, and `live` records the cursor; a cursor past the index or off the
285
+ * file's lines gets the whole prefix, checked as always.
286
+ *
252
287
  * A live file that cannot be read may have been removed by the writer after `bundle` was read: it
253
288
  * removes a superseded generation once run.json names the next one, and every live file once the
254
289
  * run finished. The loader then reads run.json again, once, through the same reader, and loads
@@ -257,15 +292,15 @@ function bundleRefusal(bundle, stream, owners) {
257
292
  export async function loadStreamTraces(source, bundle, options = {}) {
258
293
  const read = typeof source === "function" ? source : containedReader(source);
259
294
  const maxBytes = options.maxBytes ?? RUN_ARTIFACT_MAX_BYTES;
260
- let loaded = await loadEveryStream(read, bundle, maxBytes);
295
+ let loaded = await loadEveryStream(read, bundle, maxBytes, options.liveAfter);
261
296
  if (loaded.refused.some((error) => error instanceof LiveFileUnreadableError)) {
262
297
  const current = await rereadBundle(read, bundle.runId);
263
298
  if (current !== undefined)
264
- loaded = await loadEveryStream(read, current, maxBytes);
299
+ loaded = await loadEveryStream(read, current, maxBytes, options.liveAfter);
265
300
  }
266
301
  for (const file of loaded.files)
267
302
  options.onTrace?.(file.path, file.bytes);
268
- return { bundle: loaded.bundle, refused: loaded.refused };
303
+ return { bundle: loaded.bundle, refused: loaded.refused, live: loaded.live };
269
304
  }
270
305
  /** run.json as it is now, when it is a run bundle of run `runId`. */
271
306
  async function rereadBundle(read, runId) {
@@ -280,7 +315,7 @@ async function rereadBundle(read, runId) {
280
315
  return undefined;
281
316
  }
282
317
  }
283
- async function loadEveryStream(read, bundle, maxBytes) {
318
+ async function loadEveryStream(read, bundle, maxBytes, liveAfter) {
284
319
  const owners = new Map();
285
320
  for (const stream of bundle.streams)
286
321
  if (isRecord(stream.trace)) {
@@ -290,13 +325,16 @@ async function loadEveryStream(read, bundle, maxBytes) {
290
325
  const streams = [];
291
326
  const refused = [];
292
327
  const files = [];
328
+ const live = [];
293
329
  for (const stream of bundle.streams) {
294
330
  try {
295
331
  const reason = bundleRefusal(bundle, stream, owners);
296
332
  if (reason !== undefined)
297
333
  throw new StreamTraceRefusedError(stream.id, reason);
298
- const loaded = await readStreamTrace(read, stream, maxBytes);
334
+ const loaded = await readStreamTrace(read, stream, maxBytes, liveAfter);
299
335
  files.push(...loaded.files);
336
+ if (loaded.live !== undefined)
337
+ live.push(loaded.live);
300
338
  streams.push(loaded.stream);
301
339
  }
302
340
  catch (error) {
@@ -306,7 +344,7 @@ async function loadEveryStream(read, bundle, maxBytes) {
306
344
  streams.push(stream);
307
345
  }
308
346
  }
309
- return { bundle: { ...bundle, streams }, refused, files };
347
+ return { bundle: { ...bundle, streams }, refused, files, live };
310
348
  }
311
349
  /**
312
350
  * A loaded bundle as a reader that shows a run presents it. A refused stream keeps run.json's
@@ -175,6 +175,15 @@ export interface RunParticipantAssignment {
175
175
  goal: string;
176
176
  }>;
177
177
  }
178
+ /** One file humanish copied onto a participant's desktop before it started (participants[].files). */
179
+ export interface RunDesktopFile {
180
+ /** Where it landed, relative to the desktop user's home folder. */
181
+ as: string;
182
+ /** Its size in bytes, confirmed on the desktop. */
183
+ size: number;
184
+ /** The sha256 of the bytes humanish copied, 64 hex characters. */
185
+ sha256: string;
186
+ }
178
187
  export interface RunStream {
179
188
  id: string;
180
189
  simId: string;
@@ -232,6 +241,13 @@ export interface RunStream {
232
241
  };
233
242
  /** Truthful screen/window/viewport evidence for hosted desktop browser participants. */
234
243
  desktopGeometry?: RunDesktopGeometry;
244
+ /** The files copied onto this participant's desktop before it started. Never their contents. */
245
+ desktopFiles?: RunDesktopFile[];
246
+ /**
247
+ * The participant's time zone (participants[].clock, else actor.clock): its turns gave the time
248
+ * in it, and its hosted browser ran in it. Absent when it had none.
249
+ */
250
+ timeZone?: string;
235
251
  terminal?: {
236
252
  title: string;
237
253
  format: "ansi" | "plain";
@@ -6,54 +6,118 @@ import type { RunParticipantArrival } from "../run/bundle.js";
6
6
  export declare const LATEST_START_AFTER_MS: number;
7
7
  /** Whether `value` is a start offset humanish runs: whole milliseconds from 0 to 24 hours. */
8
8
  export declare function isStartAfterMs(value: unknown): value is number;
9
- /** How long before its end a participant is told the time left, unless it declares `windDownMs`. */
10
- export declare const DEFAULT_WIND_DOWN_MS: number;
11
- /** A participant's declared window: when it starts and when it ends, on the run's clock. */
12
- export interface ParticipantWindow {
13
- readonly id?: string;
9
+ /**
10
+ * When a participant starts and how its session ends, resolved once from its participants entry
11
+ * when the plan builds the participant (participantSchedule). The plan, `study check`, the bundle
12
+ * estimate and the route read a participant's time from here.
13
+ */
14
+ export interface ParticipantSchedule {
15
+ /** Its declared start, in ms after the run starts its participants; absent, with the run. */
14
16
  readonly startAfterMs?: number;
15
- readonly endAfterMs?: number;
16
- readonly windDownMs?: number;
17
+ /**
18
+ * How its session ends, when it declares `endAfterMs` or `maxSessionMs`. Without either it works
19
+ * up to the study's session budget.
20
+ */
21
+ readonly end?: ScheduleEnd;
22
+ }
23
+ /**
24
+ * A declared end. A `scheduled_end` is `endAfterMs` on the run's clock and holds the participant
25
+ * until then; a `session_cap` is `maxSessionMs` from the start of the participant's own session,
26
+ * which it may end sooner. `windowMs` is how long it may work: from its start to its end, or its
27
+ * cap. `windDownMs` is how long before the end each turn tells it the time left. `sessionMs` is its
28
+ * session budget and how long it holds a slot: the window plus SCHEDULED_END_ROOM_MS.
29
+ */
30
+ type ScheduleEnd = {
31
+ readonly windowMs: number;
32
+ readonly windDownMs: number;
33
+ readonly sessionMs: number;
34
+ } & ({
35
+ readonly kind: "scheduled_end";
36
+ readonly endAfterMs: number;
37
+ } | {
38
+ readonly kind: "session_cap";
39
+ });
40
+ /** A participant as the schedule's readers name it. */
41
+ export interface ScheduledParticipant {
42
+ readonly id: string;
43
+ readonly schedule: ParticipantSchedule;
17
44
  }
18
45
  /**
19
46
  * Why a participant's declared end cannot run, or undefined: an end that is not a whole number of
20
- * milliseconds up to 24 hours, one at or before its start, or a wind-down that is not a whole
21
- * number of milliseconds or has no end. `path` names the entry, as in "participants[1]".
47
+ * milliseconds up to 24 hours, one at or before its start, a cap that is not a whole number of
48
+ * milliseconds from 1 to 24 hours, a cap beside an end, or a wind-down that is not a whole number
49
+ * of milliseconds or has neither. `path` names the entry, as in "participants[1]".
22
50
  */
23
51
  export declare function windowReason(entry: {
24
52
  readonly id?: string;
25
53
  readonly startAfterMs?: number;
26
54
  readonly endAfterMs?: unknown;
55
+ readonly maxSessionMs?: unknown;
27
56
  readonly windDownMs?: unknown;
28
57
  }, path: string): string | undefined;
29
58
  /**
30
- * A participant's session budget: with a declared end, its window plus SCHEDULED_END_ROOM_MS in
31
- * place of the study's `sessionMs` (execution.timeoutMs or its default); else `sessionMs`.
59
+ * The schedule of a participants entry that windowReason accepts. A cap wins over an end, which
60
+ * windowReason refuses beside it anyway.
32
61
  */
33
- export declare function participantSessionMs(participant: ParticipantWindow, sessionMs: number): number;
62
+ export declare function participantSchedule(entry: {
63
+ readonly startAfterMs?: number;
64
+ readonly endAfterMs?: number;
65
+ readonly maxSessionMs?: number;
66
+ readonly windDownMs?: number;
67
+ }): ParticipantSchedule;
68
+ /**
69
+ * The schedule a rerun gives a participant: it starts with the rerun, and a scheduled end keeps its
70
+ * time on the rerun's clock and its wind-down, so its window runs from the rerun's start.
71
+ */
72
+ export declare function startingWithTheRun(schedule: ParticipantSchedule): ParticipantSchedule;
73
+ /**
74
+ * A participant's own session budget and sandbox deadline. With an end or a cap it is the end's
75
+ * `sessionMs`, and the sandbox keeps the study sandbox's headroom over the study session on top;
76
+ * without one, the study's own.
77
+ */
78
+ export declare function participantSession(schedule: ParticipantSchedule, study: {
79
+ readonly sessionMs: number;
80
+ readonly sandboxMs: number;
81
+ }): {
82
+ sessionMs: number;
83
+ sandboxMs: number;
84
+ };
34
85
  /**
35
- * When a participant that was due at `scheduledAt` (runOnSchedule's ScheduledStart) ends, on the
36
- * same clock, or undefined when it declares no end. A participant that waited for a slot still ends
37
- * at its declared time.
86
+ * When a participant's session ends: at a time on the run's clock, or at the latest a time after
87
+ * its own session starts, which the route reads when the session starts.
38
88
  */
39
- export declare function scheduledEndAt(participant: ParticipantWindow, scheduledAt: number): {
89
+ export type ParticipantEnd = {
90
+ readonly kind: "scheduled_end";
40
91
  readonly atMs: number;
41
92
  readonly windDownMs: number;
42
- } | undefined;
93
+ } | {
94
+ readonly kind: "session_cap";
95
+ readonly afterMs: number;
96
+ readonly windDownMs: number;
97
+ };
98
+ /**
99
+ * How a participant that was due at `scheduledAt` (runOnSchedule's ScheduledStart) ends, or
100
+ * undefined without an end or a cap. A scheduled end comes its window after the participant's
101
+ * time, which is its `endAfterMs` on the run's clock, even when it waited for a slot; a cap is
102
+ * whole from wherever its session starts.
103
+ */
104
+ export declare function participantEnd(schedule: ParticipantSchedule, scheduledAt: number): ParticipantEnd | undefined;
43
105
  /**
44
- * Why the sandbox of the first participant whose window passes `ceilingMs` cannot live that long,
45
- * or undefined. Its deadline is its session (participantSessionMs) plus `headroomMs`, what the
46
- * route adds to a session for setup and teardown (sandboxHeadroomMs).
106
+ * Why the sandbox of the first participant whose window or cap passes `ceilingMs` cannot live that
107
+ * long, or undefined. Its deadline is its participantSession's, from the study's session budget and
108
+ * sandbox deadline.
47
109
  */
48
- export declare function windowDeadlineRefusal(participants: readonly ParticipantWindow[], sandbox: {
110
+ export declare function windowDeadlineRefusal(participants: readonly ScheduledParticipant[], study: {
49
111
  readonly sessionMs: number;
50
- readonly headroomMs: number;
112
+ readonly sandboxMs: number;
51
113
  }, ceilingMs: number): string | undefined;
52
114
  /**
53
- * Each participant's window for `study check`, when any participant declares an end: when it starts
54
- * and ends and when it is first told the time left, or when it starts and its session budget.
115
+ * Each participant's window for `study check`, when any participant declares an end or a cap: when
116
+ * it starts and ends and when it is first told the time left, when it starts and its cap and how
117
+ * far into its session it is first told the time left, or when it starts and the study's
118
+ * `sessionMs`.
55
119
  */
56
- export declare function describeWindows(participants: readonly ParticipantWindow[], sessionMs: number): string | undefined;
120
+ export declare function describeWindows(participants: readonly ScheduledParticipant[], sessionMs: number): string | undefined;
57
121
  /** The refusal of a start offset that is not one, naming its `field`. */
58
122
  export declare function startAfterMessage(field: string): string;
59
123
  /**
@@ -94,13 +158,13 @@ export interface StartQueue {
94
158
  readonly ownSlot?: number;
95
159
  }
96
160
  /**
97
- * The schedule a roster's declared offsets give under the queue (`undefined` is 0: the participant
98
- * starts with the run). Participants start in schedule order, by offset and then roster position,
99
- * each at its time or when a slot frees, whichever is later; this is the rule runOnSchedule
100
- * follows. `sessionsMs` gives a participant with a declared end its own session budget
101
- * (participantSessionMs); the others hold their slot for `queue.sessionMs`.
161
+ * The schedule a roster's participants give under the queue. Participants start in schedule order,
162
+ * by offset and then roster position, each at its time or when a slot frees, whichever is later;
163
+ * this is the rule runOnSchedule follows. A participant with an end or a cap holds its slot for its
164
+ * own session budget, and the others for `queue.sessionMs`. A participant whose slot would free
165
+ * only at or after its scheduled end takes none: the run skips it (pastEndReason).
102
166
  */
103
- export declare function planArrivals(offsets: readonly (number | undefined)[], queue: StartQueue, sessionsMs?: readonly number[]): ArrivalPlan;
167
+ export declare function planArrivals(schedules: readonly ParticipantSchedule[], queue: StartQueue): ArrivalPlan;
104
168
  /**
105
169
  * The plan's schedule, after a "schedule:" label: the first and last start, and how many run at
106
170
  * once at the most.
@@ -110,13 +174,19 @@ export declare function describeArrivals(arrivals: ArrivalPlan): string;
110
174
  * A planner's arrival plan for its participants under the start queue, and the warnings the plan
111
175
  * records about it. A participant's desktop is created when it starts, so on computer use a late
112
176
  * start adds nothing to any sandbox's deadline; the provisioned shared world checks its app's
113
- * sandbox with servedScheduleRefusal. `lowered` is the warning that the E2B plan's limit runs the
114
- * roster in waves.
177
+ * sandbox with servedScheduleRefusal. `lowered` is participantsAtOnce's warning that the E2B plan's
178
+ * limit or HUMANISH_CUA_MAX_CONCURRENCY runs the roster in waves.
115
179
  */
116
- export declare function planStarts(participants: readonly ParticipantWindow[], queue: StartQueue, lowered: string | undefined): {
180
+ export declare function planStarts(participants: readonly ScheduledParticipant[], queue: StartQueue, lowered: string | undefined): {
117
181
  arrivals: ArrivalPlan;
118
182
  warnings: string[];
119
183
  };
184
+ /**
185
+ * Why a participant whose start comes at `nowMs` is skipped, or undefined: its scheduled end came
186
+ * while it waited, so its session would end before it began. The plan's simulation applies the
187
+ * same rule (planArrivals).
188
+ */
189
+ export declare function pastEndReason(end: ParticipantEnd | undefined, nowMs: number): string | undefined;
120
190
  /**
121
191
  * Why a sandbox that serves the app until the last participant ends, the provisioned shared
122
192
  * world's subject sandbox, cannot live that long under `ceilingMs`; undefined when it can. Its
@@ -159,3 +229,4 @@ export declare function runOnSchedule<T, R>(items: readonly T[], options: {
159
229
  readonly sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
160
230
  readonly signal?: AbortSignal;
161
231
  }, start: (item: T, index: number, scheduled: ScheduledStart) => Promise<R>): Promise<R[]>;
232
+ export {};