humanish 0.118.0 → 0.120.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 (80) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/analysis/admission.d.ts +16 -2
  3. package/dist/analysis/admission.js +35 -6
  4. package/dist/analysis/analysis-limits.d.ts +32 -0
  5. package/dist/analysis/analysis-limits.js +41 -3
  6. package/dist/analysis/automatic-config.d.ts +5 -0
  7. package/dist/analysis/automatic-config.js +15 -1
  8. package/dist/analysis/cohorts.d.ts +103 -0
  9. package/dist/analysis/cohorts.js +80 -0
  10. package/dist/analysis/evidence.d.ts +5 -10
  11. package/dist/analysis/evidence.js +37 -34
  12. package/dist/analysis/execute.d.ts +7 -2
  13. package/dist/analysis/execute.js +242 -297
  14. package/dist/analysis/provider.js +8 -1
  15. package/dist/analysis/responses.d.ts +19 -0
  16. package/dist/analysis/responses.js +201 -0
  17. package/dist/analysis/store.js +2 -1
  18. package/dist/analysis/validation.js +13 -7
  19. package/dist/cli/commands/analyze.js +8 -3
  20. package/dist/cli/commands/study-format.js +46 -7
  21. package/dist/cli/commands/study-route-computer-use.js +2 -2
  22. package/dist/cli/commands/study-route-scripted.js +2 -2
  23. package/dist/cli/commands/study-route-shared-world.js +2 -2
  24. package/dist/cli/commands/study-route-terminal.js +2 -2
  25. package/dist/cli/commands/study-run.js +3 -3
  26. package/dist/cli/commands/study.js +2 -2
  27. package/dist/cli/io.js +2 -3
  28. package/dist/observer-app.html +2 -2
  29. package/dist/routes/computer-use/fanout-bundle.js +2 -1
  30. package/dist/routes/computer-use/fanout-records.js +4 -0
  31. package/dist/routes/computer-use/participant-execution.d.ts +6 -4
  32. package/dist/routes/computer-use/participant-execution.js +42 -14
  33. package/dist/routes/computer-use/participant-model.js +18 -8
  34. package/dist/routes/computer-use/participant-runs.js +12 -0
  35. package/dist/routes/computer-use/plan.js +23 -15
  36. package/dist/routes/computer-use/rerun-selection.js +14 -3
  37. package/dist/routes/computer-use/types.d.ts +13 -1
  38. package/dist/routes/shared-world/bundle-records.js +14 -0
  39. package/dist/routes/shared-world/bundle.js +2 -1
  40. package/dist/routes/shared-world/external-public.js +15 -3
  41. package/dist/routes/shared-world/handoff.d.ts +2 -2
  42. package/dist/routes/shared-world/plan.d.ts +7 -0
  43. package/dist/routes/shared-world/plan.js +79 -34
  44. package/dist/routes/shared-world/provisioned.js +15 -8
  45. package/dist/routes/shared-world/route.js +5 -2
  46. package/dist/routes/shared-world/types.d.ts +2 -0
  47. package/dist/run/bundle-shape.js +10 -1
  48. package/dist/run/bundle.d.ts +15 -0
  49. package/dist/run/participant-records.js +1 -0
  50. package/dist/run/route-shell.js +5 -10
  51. package/dist/run/run.js +4 -2
  52. package/dist/study/arrivals.d.ts +114 -0
  53. package/dist/study/arrivals.js +204 -0
  54. package/dist/study/doctor.js +2 -15
  55. package/dist/study/keys.d.ts +1 -0
  56. package/dist/study/keys.js +2 -0
  57. package/dist/study/migrate/v2.js +3 -2
  58. package/dist/study/parse/actors.d.ts +1 -1
  59. package/dist/study/parse/actors.js +9 -3
  60. package/dist/study/parse/front.js +29 -1
  61. package/dist/study/plan-base.d.ts +1 -3
  62. package/dist/study/plan-base.js +0 -3
  63. package/dist/study/plan-participants.d.ts +2 -0
  64. package/dist/study/plan-participants.js +1 -0
  65. package/dist/study/plan-types.d.ts +8 -3
  66. package/dist/study/preflight-result.js +2 -2
  67. package/dist/study/preflight.js +12 -0
  68. package/dist/study/summary.d.ts +3 -3
  69. package/dist/study/summary.js +5 -3
  70. package/dist/study/types.d.ts +14 -0
  71. package/dist/study/validation.js +5 -0
  72. package/dist/study/warnings.js +11 -3
  73. package/docs/contracts/run-bundle.md +1 -1
  74. package/docs/contracts/schemas.md +19 -1
  75. package/docs/contracts/study-analysis.md +59 -6
  76. package/docs/principles/invariants-and-defaults.md +14 -14
  77. package/docs/product/automatic-analysis.md +7 -1
  78. package/package.json +1 -1
  79. package/dist/study/automatic-analysis-plan.d.ts +0 -28
  80. package/dist/study/automatic-analysis-plan.js +0 -57
@@ -14,6 +14,7 @@ import { concurrentSharedWorldValidationReason, desktopMediaValidationReason, ex
14
14
  import { unpricedCapCheck } from "../../study/requirements.js";
15
15
  import { participantsAtOnce, sandboxDeadlineRefusal, sandboxHeadroomMs, } from "../../substrates/e2b/lifetime.js";
16
16
  import { boundedConcurrency } from "../computer-use/participant-runs.js";
17
+ import { describeArrivals, planStarts, servedScheduleRefusal, } from "../../study/arrivals.js";
17
18
  /** The registered descriptor for a planned actor; planSharedWorldStudy checked the registry. */
18
19
  export function sharedWorldDescriptorOf(actorType) {
19
20
  const descriptor = actorRegistry[actorType];
@@ -96,53 +97,31 @@ export function planSharedWorldStudy(config, input) {
96
97
  const plane = planeOf(config);
97
98
  if (plane === undefined)
98
99
  throw new Error("shared-world validation admitted a plane it cannot plan");
99
- // Every participant's desktop and, on the provisioned plane, the app's own sandbox share the E2B
100
- // plan's concurrent sandboxes. A shared world needs two participants live together.
101
- const declared = config.execution?.concurrency;
102
- const n = plane.participants.length;
103
- const atOnce = participantsAtOnce({
104
- concurrency: boundedConcurrency(declared, n),
105
- declared: declared !== undefined,
106
- participants: n,
107
- alongside: plane.kind === "provisioned" ? 1 : 0,
108
- minimum: 2,
109
- }, input.concurrentSandboxes);
110
- if (!atOnce.ok)
111
- return refuse(invalid, atOnce.message, actor);
112
100
  const sessionTimeoutMs = config.execution?.timeoutMs ?? defaultSessionTimeoutMs(plane, ceiling.ms);
113
- // The run's longest deadline: on the provisioned plane the subject sandbox, which serves the app
114
- // until every participant ends; on the external-public plane each participant's own sandbox.
115
- const deadlineReason = sandboxDeadlineRefusal(plane.kind === "provisioned"
116
- ? {
117
- name: "the subject sandbox, which serves the app until every participant ends",
118
- sessionMs: sessionTimeoutMs,
119
- sessionDeclared: config.execution?.timeoutMs !== undefined,
120
- servedSubject: { seed: plane.subject.state.seed ?? [] },
121
- }
122
- : {
123
- name: "each participant's sandbox",
124
- sessionMs: sessionTimeoutMs,
125
- sessionDeclared: config.execution?.timeoutMs !== undefined,
126
- }, ceiling.ms);
101
+ const starts = startsOf(plane, config.execution?.concurrency, input.concurrentSandboxes, sessionTimeoutMs);
102
+ if (!starts.ok)
103
+ return refuse(invalid, starts.message, actor);
104
+ const { arrivals, warnings } = starts;
105
+ const deadlineReason = deadlineReasonOf(plane, arrivals, {
106
+ sessionDeclared: config.execution?.timeoutMs !== undefined,
107
+ ceilingMs: ceiling.ms,
108
+ });
127
109
  if (deadlineReason)
128
110
  return refuse(invalid, deadlineReason, actor);
129
111
  const brain = brainOf(config, false);
130
112
  if (brain.kind === "caller")
131
113
  throw new Error("a shared-world participant has no caller brain");
132
- const base = planBase(config, {
133
- dryRun: input.dryRun,
134
- analysis,
135
- participants: n,
136
- });
114
+ const base = planBase(config, { dryRun: input.dryRun, analysis });
137
115
  return {
138
116
  ok: true,
139
117
  plan: {
140
118
  ...base,
141
- ...(atOnce.lowered === undefined ? {} : { warnings: [atOnce.lowered] }),
119
+ ...(warnings.length === 0 ? {} : { warnings }),
142
120
  route: "shared-world",
143
121
  actor,
144
122
  plane,
145
- concurrency: atOnce.count,
123
+ concurrency: starts.count,
124
+ arrivals,
146
125
  sessionTimeoutMs,
147
126
  brain,
148
127
  caps: planCaps(config),
@@ -164,6 +143,72 @@ export function planSharedWorldStudy(config, input) {
164
143
  },
165
144
  };
166
145
  }
146
+ /**
147
+ * How many participants run at once, and when each one starts. Every participant's desktop and, on
148
+ * the provisioned plane, the app's own sandbox share the E2B plan's concurrent sandboxes, and a
149
+ * shared world needs two participants live together. On the external-public plane the host runs
150
+ * on a slot of its own and the others queue for the rest (external-public.ts).
151
+ */
152
+ function startsOf(plane, declared, limit, sessionMs) {
153
+ const n = plane.participants.length;
154
+ const atOnce = participantsAtOnce({
155
+ concurrency: boundedConcurrency(declared, n),
156
+ declared: declared !== undefined,
157
+ participants: n,
158
+ alongside: plane.kind === "provisioned" ? 1 : 0,
159
+ minimum: 2,
160
+ }, limit);
161
+ if (!atOnce.ok)
162
+ return atOnce;
163
+ const hostIndex = plane.participants.findIndex((participant) => participant.host === true);
164
+ const queue = {
165
+ slots: atOnce.count,
166
+ sessionMs,
167
+ ...(plane.kind === "external-public" && hostIndex >= 0 ? { ownSlot: hostIndex } : {}),
168
+ };
169
+ return {
170
+ ok: true,
171
+ count: atOnce.count,
172
+ ...planStarts(plane.participants, queue, atOnce.lowered),
173
+ };
174
+ }
175
+ /**
176
+ * Why the run's longest-lived sandbox cannot live as long as the plan needs. On the provisioned
177
+ * plane that is the subject sandbox, which serves the app for one session and then until the last
178
+ * participant ends; on the external-public plane, each participant's own sandbox, created at its
179
+ * start.
180
+ */
181
+ function deadlineReasonOf(plane, arrivals, options) {
182
+ const { sessionDeclared, ceilingMs } = options;
183
+ const sessionMs = arrivals.sessionMs;
184
+ if (plane.kind !== "provisioned")
185
+ return sandboxDeadlineRefusal({ name: "each participant's sandbox", sessionMs, sessionDeclared }, ceilingMs);
186
+ const seed = plane.subject.state.seed ?? [];
187
+ return (sandboxDeadlineRefusal({
188
+ name: "the subject sandbox, which serves the app until every participant ends",
189
+ sessionMs,
190
+ sessionDeclared,
191
+ servedSubject: { seed },
192
+ }, ceilingMs) ?? servedScheduleRefusal(arrivals, { sessionMs, seed }, ceilingMs));
193
+ }
194
+ /**
195
+ * The schedule lines a shared-world run prints before it creates anything, when the study declares
196
+ * `participants[].startAfterMs`: the schedule, and the worst case in sandbox-minutes, which counts
197
+ * every participant's sandbox (its session plus the teardown buffer) and on the provisioned plane
198
+ * the app's sandbox until the last participant ends.
199
+ */
200
+ export function sharedWorldScheduleLines(plan) {
201
+ const participants = plan.plane.participants.length;
202
+ const participantMs = participants * (plan.sessionTimeoutMs + sandboxHeadroomMs());
203
+ const subjectMs = plan.plane.kind === "provisioned"
204
+ ? plan.arrivals.lastEndMs + sandboxHeadroomMs({ seed: plan.plane.subject.state.seed ?? [] })
205
+ : 0;
206
+ return [
207
+ `humanish shared-world plan (${plan.studyId}): ${participants} participants, at most ${plan.concurrency} at once; worst-case ~${Math.round((participantMs + subjectMs) / 60_000)} sandbox-minutes total${plan.dryRun ? " (dry-run: $0)" : ""}.`,
208
+ ` schedule: ${describeArrivals(plan.arrivals)}`,
209
+ "",
210
+ ].join("\n");
211
+ }
167
212
  // The default per-participant session budget is derived from the plane. On a provisioned plane the
168
213
  // binding constraint is the subject sandbox (it must outlive every participant: timeoutMs +
169
214
  // provisioning + seeding + teardown buffer, under the sandbox ceiling), so the derivation hands each
@@ -6,7 +6,7 @@ import { buildOriginMap } from "../../comms/capture-surface.js";
6
6
  import { deployCommsCatch, refreshInboxSurface, writeInboxSurface, } from "../../comms/sandbox-catch.js";
7
7
  import { redactText, toErrorMessage } from "../../evidence/redaction.js";
8
8
  import { liveObserverResult } from "../../observer/live.js";
9
- import { mapWithConcurrency } from "../../run/concurrency.js";
9
+ import { runOnSchedule } from "../../study/arrivals.js";
10
10
  import { provisionCloneSubject } from "../../subject/clone.js";
11
11
  import { provisionLocalTreeSubject } from "../../subject/local-tree.js";
12
12
  import { defaultSharedWorldPhaseSink } from "../../subject/steps.js";
@@ -96,7 +96,7 @@ class SubjectPlane {
96
96
  this.setup.stateSnapshots.push({ timestamp, digest: snapshot.digest });
97
97
  }
98
98
  async acquire() {
99
- const { plan, deps, env, requestTimeoutMs, timeoutMs } = this.ctx;
99
+ const { plan, deps, env, requestTimeoutMs } = this.ctx;
100
100
  const { subjectEnvNames, commsEnv } = this.setup;
101
101
  const subjectModule = await (deps.desktopModule ?? loadE2BDesktopModule)();
102
102
  // The one subject sandbox: headless service host (no GUI participant). The subject env is
@@ -107,7 +107,9 @@ class SubjectPlane {
107
107
  module: subjectModule,
108
108
  apiKey: this.ctx.e2bApiKey,
109
109
  requestTimeoutMs,
110
- sessionTimeoutMs: timeoutMs,
110
+ // The app serves until the last participant ends, which a late start or a wait for a free
111
+ // slot puts past one session.
112
+ sessionTimeoutMs: plan.arrivals.lastEndMs,
111
113
  seed: planeStateOf(plan)?.seed ?? [],
112
114
  metadata: {
113
115
  ...CONCURRENT_SHARED_WORLD_PROVIDER_METADATA,
@@ -380,15 +382,20 @@ async function publishInProgress(plane, ctx, live, setup) {
380
382
  }
381
383
  startParticipantFlush(ctx, live, inProgressBundle);
382
384
  }
383
- // Launch N actor sandboxes concurrently and independently (runCuaParticipant + mapWithConcurrency;
384
- // runCuaParticipants would add a pipeline gate and fail-fast). Each actor's window is measured on the
385
- // one orchestrator clock.
385
+ // Launch N actor sandboxes independently on the study's schedule (runCuaParticipant +
386
+ // runOnSchedule; runCuaParticipants would add a pipeline gate and fail-fast). Each actor's window is
387
+ // measured on the one orchestrator clock.
386
388
  function runParticipants(plane, ctx, live, setup) {
387
389
  const { now } = ctx;
388
390
  const participants = ctx.plan.plane.participants;
389
391
  const { commsEmail } = setup;
390
392
  const baseActorDeps = participantRunDeps(ctx, live, ctx.scrubKnownValues);
391
- return mapWithConcurrency(ctx.actorSpecs, Math.max(1, ctx.concurrency), async (spec, i) => {
393
+ const schedule = {
394
+ startAfterMs: (spec) => spec.planned.startAfterMs,
395
+ slots: Math.max(1, ctx.concurrency),
396
+ now,
397
+ };
398
+ return runOnSchedule(ctx.actorSpecs, schedule, async (spec, i, { scheduledAt }) => {
392
399
  const route = resolveActorEntryUrl(plane.getHostUrl, participants[i]?.entry);
393
400
  // Tell this persona its (getHost-reachable) inbox URL, but only when comms is live and this participant
394
401
  // has a declared recipient it can actually receive mail into (else it would stall on an empty
@@ -400,7 +407,7 @@ function runParticipants(plane, ctx, live, setup) {
400
407
  const startedAt = now();
401
408
  const outcome = await runCuaParticipant(actorSpec, { ...baseActorDeps, appUrl: route });
402
409
  const endedAt = now();
403
- return { spec, outcome, startedAt, endedAt, route };
410
+ return { spec, outcome, scheduledAt, startedAt, endedAt, route };
404
411
  });
405
412
  }
406
413
  export async function runProvisionedPlane(ctx, live, setup) {
@@ -23,7 +23,7 @@
23
23
  // per-delta→actor field, so causation under concurrency is structurally inexpressible.
24
24
  //
25
25
  // Capability and proof: the deterministic $0 gate proves the plumbing + the claims-match-mechanism
26
- // contract: the real mapWithConcurrency produces genuinely overlapping laneWindows (a rendezvous
26
+ // contract: the real start queue (runOnSchedule) produces genuinely overlapping laneWindows (a rendezvous
27
27
  // latch in the fake session forces two participant fns in-flight while the real orchestrator clock
28
28
  // measures the windows). Every generated bundle describes only its own observations; no one run
29
29
  // establishes scale, repeatability, or adopter-harness replacement.
@@ -38,7 +38,7 @@ import {} from "../../run/run.js";
38
38
  import { RunSecrets } from "../../run/secrets.js";
39
39
  import { admitRoute, completeRefusalAnalysis, } from "../../run/route-shell.js";
40
40
  import { runExternalPublicPlane } from "./external-public.js";
41
- import { sharedWorldDescriptorOf } from "./plan.js";
41
+ import { sharedWorldDescriptorOf, sharedWorldScheduleLines, } from "./plan.js";
42
42
  import { runProvisionedPlane } from "./provisioned.js";
43
43
  import { concurrentStudyFailure, finishConcurrentRun } from "./result.js";
44
44
  import { prepareConcurrentRun } from "./setup.js";
@@ -143,6 +143,9 @@ async function runPlanInScope(plan, input, config, scope) {
143
143
  const deps = input.deps ?? {};
144
144
  const env = input.env ?? process.env;
145
145
  const fail = sharedWorldFailure(plan, input);
146
+ // Before any sandbox or provider call, as computer use prints its participant plan.
147
+ if (plan.arrivals.declared)
148
+ process.stderr.write(sharedWorldScheduleLines(plan));
146
149
  const descriptor = sharedWorldDescriptorOf(plan.actor);
147
150
  const planeClass = plan.plane.kind === "external-public" ? "external-public" : "provisioned-getHost";
148
151
  // The provisioned plane's subject; the external-public plane has none.
@@ -138,6 +138,8 @@ export interface ConcurrentSharedWorldStudyResult extends AutomaticAnalysisResul
138
138
  export interface ActorRunResult {
139
139
  spec: DesktopParticipantRun;
140
140
  outcome: ParticipantRunOutcome;
141
+ /** When the schedule had it start, in epoch ms; the host of an external public app starts with the run. */
142
+ scheduledAt: number;
141
143
  startedAt: number;
142
144
  endedAt: number;
143
145
  route: string;
@@ -313,7 +313,16 @@ function isRunSimulation(value) {
313
313
  Array.isArray(value.streamIds) &&
314
314
  value.streamIds.every((streamId) => typeof streamId === "string") &&
315
315
  typeof value.startedAt === "string" &&
316
- typeof value.updatedAt === "string");
316
+ typeof value.updatedAt === "string" &&
317
+ (value.arrival === undefined || isRunParticipantArrival(value.arrival)));
318
+ }
319
+ function isRunParticipantArrival(value) {
320
+ return (isRecord(value) &&
321
+ typeof value.startAfterMs === "number" &&
322
+ Number.isSafeInteger(value.startAfterMs) &&
323
+ value.startAfterMs >= 0 &&
324
+ (value.scheduledAt === undefined || typeof value.scheduledAt === "string") &&
325
+ (value.startedAt === undefined || typeof value.startedAt === "string"));
317
326
  }
318
327
  function isRunAdapterArtifact(value) {
319
328
  return (isRecord(value) &&
@@ -123,6 +123,21 @@ export interface RunSimulation {
123
123
  streamIds: string[];
124
124
  startedAt: string;
125
125
  updatedAt: string;
126
+ /**
127
+ * When a computer-use or shared-world participant was due and when it started, in a study that
128
+ * declares `participants[].startAfterMs`. `startAfterMs` is its offset (0 when its entry declares
129
+ * none), `scheduledAt` the moment the run started its participants plus that offset, and
130
+ * `startedAt` when its desktop was requested: later than `scheduledAt` when it waited for a free
131
+ * slot. A dry run records only the offset, and a participant that never started has no
132
+ * `startedAt`. The record's own `startedAt` above is the run's creation time.
133
+ */
134
+ arrival?: RunParticipantArrival;
135
+ }
136
+ /** A participant record's `arrival`. */
137
+ export interface RunParticipantArrival {
138
+ startAfterMs: number;
139
+ scheduledAt?: string;
140
+ startedAt?: string;
126
141
  }
127
142
  export interface RunEvent {
128
143
  id: string;
@@ -29,6 +29,7 @@ export function participantRecord(ids, place, fields) {
29
29
  streamIds: [ids.streamId],
30
30
  startedAt: fields.startedAt,
31
31
  updatedAt: fields.updatedAt,
32
+ ...(fields.arrival === undefined ? {} : { arrival: fields.arrival }),
32
33
  };
33
34
  }
34
35
  /**
@@ -4,7 +4,6 @@
4
4
  // either is made here once. The preview route records no run and has no shell.
5
5
  import { completeAutomaticAnalysis } from "../analysis/automatic-completion.js";
6
6
  import { resolveAutomaticAnalysis } from "../analysis/automatic-config.js";
7
- import { withParticipantLimitSkip } from "../study/automatic-analysis-plan.js";
8
7
  import { runScope } from "./run.js";
9
8
  import { withTransientCommsSecrets } from "./transient-comms-secrets.js";
10
9
  /**
@@ -13,15 +12,11 @@ import { withTransientCommsSecrets } from "./transient-comms-secrets.js";
13
12
  * it takes closes the run it started, and only a run that published its final bundle is analyzed.
14
13
  */
15
14
  export async function admitRoute(shell) {
16
- const complete = (input, result, finished) =>
17
- // A live run of more participants than one analysis reads records the skip and starts nothing.
18
- shell.analysis?.skip !== undefined && !result.dryRun
19
- ? Promise.resolve(withParticipantLimitSkip(result, shell.analysis.trigger))
20
- : completeAutomaticAnalysis(result, finished, shell.analysis?.config, input, {
21
- ...(shell.analysis === undefined ? {} : { trigger: shell.analysis.trigger }),
22
- preferLargerOutput: shell.analysis?.preferLargerOutput === true,
23
- ...(shell.analysisRefusal === undefined ? {} : { refusal: shell.analysisRefusal }),
24
- });
15
+ const complete = (input, result, finished) => completeAutomaticAnalysis(result, finished, shell.analysis?.config, input, {
16
+ ...(shell.analysis === undefined ? {} : { trigger: shell.analysis.trigger }),
17
+ preferLargerOutput: shell.analysis?.preferLargerOutput === true,
18
+ ...(shell.analysisRefusal === undefined ? {} : { refusal: shell.analysisRefusal }),
19
+ });
25
20
  const admission = await shell.admit();
26
21
  if (!admission.ok)
27
22
  return {
package/dist/run/run.js CHANGED
@@ -117,13 +117,15 @@ function observerTarget(options) {
117
117
  function runPublisher(args) {
118
118
  const { options, runId, createdAt, paths, runStatus, now, admit, participantsRan } = args;
119
119
  const observer = observerTarget(options);
120
- // The study's warnings join whatever events the route wrote, once, in every write.
120
+ // The study's warnings join whatever events the route wrote, once, in every write. A warning can
121
+ // quote the study file, which may name a sandbox URL a participant needs, so each goes through
122
+ // the redaction a run failure gets.
121
123
  const studyWarnings = (options.warnings ?? []).map((message, index) => ({
122
124
  id: `event-study-warning-${String(index + 1).padStart(3, "0")}`,
123
125
  at: createdAt,
124
126
  level: "warn",
125
127
  type: "study.warning",
126
- message,
128
+ message: redactText(message),
127
129
  }));
128
130
  const withStudyWarnings = (events) => [
129
131
  ...events.filter((event) => !studyWarnings.some((warning) => warning.id === event.id)),
@@ -0,0 +1,114 @@
1
+ import type { RunParticipantArrival } from "../run/bundle.js";
2
+ /**
3
+ * The latest start a study may declare: 24 hours, the longest sandbox lifetime E2B documents. It
4
+ * also keeps every wait under Node's timer limit of 2^31 - 1 ms, past which a timer fires at once.
5
+ */
6
+ export declare const LATEST_START_AFTER_MS: number;
7
+ /** Whether `value` is a start offset humanish runs: whole milliseconds from 0 to 24 hours. */
8
+ export declare function isStartAfterMs(value: unknown): value is number;
9
+ /** The refusal of a start offset that is not one, naming its `field`. */
10
+ export declare function startAfterMessage(field: string): string;
11
+ /**
12
+ * Why a roster's declared starts cannot run, or undefined. A start orders participants against
13
+ * each other, so it needs two or more of them, and the host of an external public app opens the
14
+ * session the others join, so it starts with the run.
15
+ */
16
+ export declare function rosterStartsReason(roster: readonly {
17
+ readonly id?: string;
18
+ readonly startAfterMs?: number;
19
+ readonly host?: boolean;
20
+ }[]): string | undefined;
21
+ /** What the plan says about when a run's participants start, with every session at its full budget. */
22
+ export interface ArrivalPlan {
23
+ /** Whether any participant declares `startAfterMs`. */
24
+ readonly declared: boolean;
25
+ /** The earliest and the latest declared start, in ms after the run starts its participants. */
26
+ readonly firstStartMs: number;
27
+ readonly lastStartMs: number;
28
+ /** The most participants running at once. */
29
+ readonly peak: number;
30
+ /** How many participants wait past their time for a free slot, and the longest of those waits. */
31
+ readonly waiting: number;
32
+ readonly longestWaitMs: number;
33
+ /** When the last session ends at the latest, in ms after the run starts its participants. */
34
+ readonly lastEndMs: number;
35
+ /** The session budget every figure above assumes each participant uses. */
36
+ readonly sessionMs: number;
37
+ }
38
+ /** How the runtime starts participants: how many run at once, and for how long at most. */
39
+ export interface StartQueue {
40
+ readonly slots: number;
41
+ readonly sessionMs: number;
42
+ /**
43
+ * The roster index of a participant that runs on a slot of its own, outside the queue: the
44
+ * external-public host, which the others wait on. It counts against `slots`.
45
+ */
46
+ readonly ownSlot?: number;
47
+ }
48
+ /**
49
+ * The schedule a roster's declared offsets give under the queue (`undefined` is 0: the participant
50
+ * starts with the run). Participants start in schedule order, by offset and then roster position,
51
+ * each at its time or when a slot frees, whichever is later; this is the rule runOnSchedule
52
+ * follows.
53
+ */
54
+ export declare function planArrivals(offsets: readonly (number | undefined)[], queue: StartQueue): ArrivalPlan;
55
+ /**
56
+ * The plan's schedule, after a "schedule:" label: the first and last start, and how many run at
57
+ * once at the most.
58
+ */
59
+ export declare function describeArrivals(arrivals: ArrivalPlan): string;
60
+ /**
61
+ * A planner's arrival plan for its participants under the start queue, and the warnings the plan
62
+ * records about it. A participant's desktop is created when it starts, so on computer use a late
63
+ * start adds nothing to any sandbox's deadline; the provisioned shared world checks its app's
64
+ * sandbox with servedScheduleRefusal. `lowered` is the warning that the E2B plan's limit runs the
65
+ * roster in waves.
66
+ */
67
+ export declare function planStarts(participants: readonly {
68
+ readonly startAfterMs?: number;
69
+ }[], queue: StartQueue, lowered: string | undefined): {
70
+ arrivals: ArrivalPlan;
71
+ warnings: string[];
72
+ };
73
+ /**
74
+ * Why a sandbox that serves the app until the last participant ends, the provisioned shared
75
+ * world's subject sandbox, cannot live that long under `ceilingMs`; undefined when it can. Its
76
+ * deadline is the schedule's `lastEndMs` plus the time to provision and seed the subject and tear
77
+ * it down. The session alone is checked first (sandboxDeadlineRefusal), so this names what the
78
+ * schedule adds: a late start or a wait for a free slot.
79
+ */
80
+ export declare function servedScheduleRefusal(arrivals: ArrivalPlan, sandbox: {
81
+ readonly sessionMs: number;
82
+ readonly seed: readonly {
83
+ readonly timeoutMs?: number | undefined;
84
+ }[];
85
+ }, ceilingMs: number): string | undefined;
86
+ /** When a participant was due and when it started, in epoch ms. One that never started has no `startedAt`. */
87
+ export interface ParticipantArrival {
88
+ readonly scheduledAt: number;
89
+ readonly startedAt?: number;
90
+ }
91
+ /** A participant's `arrival` in run.json: its offset, and on a live run when it was due and started. */
92
+ export declare function arrivalRecord(startAfterMs: number | undefined, arrival: ParticipantArrival | undefined): RunParticipantArrival;
93
+ /** Where a participant sits in the run's schedule when its start comes. */
94
+ export interface ScheduledStart {
95
+ /** Its place in schedule order, from 0: the first participant to start has 0. */
96
+ readonly order: number;
97
+ /** Its time in epoch ms: the moment the run started its participants plus its offset. */
98
+ readonly scheduledAt: number;
99
+ }
100
+ /**
101
+ * Calls `start` for each item at its time, with at most `slots` calls in flight. Items start in
102
+ * schedule order; one whose time comes while every slot is busy waits for the next free slot. An
103
+ * abort of `signal` ends every wait at once, so `start` runs for the waiting items right away and
104
+ * decides whether to skip them. Results come back in the items' order.
105
+ */
106
+ export declare function runOnSchedule<T, R>(items: readonly T[], options: {
107
+ readonly startAfterMs: (item: T) => number | undefined;
108
+ readonly slots: number;
109
+ /** The run's clock in epoch ms, which must advance while a start waits. Defaults to Date.now. */
110
+ readonly now?: () => number;
111
+ /** Waits `ms` or until `signal` aborts. Defaults to node:timers/promises' setTimeout. */
112
+ readonly sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
113
+ readonly signal?: AbortSignal;
114
+ }, start: (item: T, index: number, scheduled: ScheduledStart) => Promise<R>): Promise<R[]>;