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
@@ -10,15 +10,14 @@ import { redactText } from "../../evidence/redaction.js";
10
10
  import {} from "../../run/bundle.js";
11
11
  import { participantAssignment } from "../../study/participant-assignment.js";
12
12
  import { resolveCommittedPersonas } from "../../study/persona-resolve.js";
13
- import { CUA_FANOUT_STRATEGY, CUA_MAX_CONCURRENCY_ENV, DEFAULT_APP_URL_SESSION_TIMEOUT_MS, MIN_DERIVED_SESSION_TIMEOUT_MS, } from "./types.js";
14
- import { sandboxHeadroomMs } from "../../substrates/e2b/lifetime.js";
15
- import { readPositiveInt } from "../../study/parse/values.js";
13
+ import { CUA_FANOUT_STRATEGY, DEFAULT_APP_URL_SESSION_TIMEOUT_MS, MIN_DERIVED_SESSION_TIMEOUT_MS, } from "./types.js";
14
+ import { CUA_MAX_CONCURRENCY_ENV, sandboxHeadroomMs } from "../../substrates/e2b/lifetime.js";
16
15
  import { digestUrl } from "./bundle-parts.js";
17
16
  import { composeParticipantInstructions, DEFAULT_MISSION } from "./participant-prompt.js";
18
17
  import { resolveCuaRerunSelection } from "./rerun-selection.js";
19
18
  import { plural } from "../../run/text.js";
20
19
  import { formatDuration } from "../../run/projection.js";
21
- import { describeArrivals, participantSessionMs, planArrivals, scheduledEndAt, } from "../../study/arrivals.js";
20
+ import { describeArrivals, participantEnd, participantSession, } from "../../study/arrivals.js";
22
21
  /**
23
22
  * The session budget of a study that declares no execution.timeoutMs. On a provisioned route it is
24
23
  * what the `ceilingMs` sandbox ceiling leaves after provisioning, seeding and the teardown buffer.
@@ -45,28 +44,21 @@ export function resolveParticipantSandboxMs(config, ceilingMs) {
45
44
  timeoutMs +
46
45
  sandboxHeadroomMs(provisionedRoute ? { seed: config.subject.state?.seed ?? [] } : undefined));
47
46
  }
48
- /**
49
- * A participant's own session budget and sandbox deadline: the study's, or with a declared end its
50
- * window plus room for the closing report (participantSessionMs), with the same setup and
51
- * teardown headroom the study's sandbox has over its session.
52
- */
53
- function participantWindow(participant, study) {
54
- const timeoutMs = participantSessionMs(participant, study.sessionBudgetMs);
55
- return { timeoutMs, sandboxMs: study.sandboxMs - study.sessionBudgetMs + timeoutMs };
56
- }
57
47
  /**
58
48
  * The deps one participant runs with once its start comes: its own session budget and sandbox
59
- * deadline, and with a declared end, that end on the run's clock (`scheduledAt` is when it was
60
- * due, from runOnSchedule).
49
+ * deadline (participantSession), and with a declared end or cap, how its session ends
50
+ * (`scheduledAt` is when it was due, from runOnSchedule).
61
51
  */
62
52
  export function scheduledParticipantDeps(participant, deps, scheduledAt) {
63
- const end = scheduledEndAt(participant, scheduledAt);
53
+ const own = participantSession(participant.schedule, {
54
+ sessionMs: deps.timeoutMs,
55
+ sandboxMs: deps.sandboxMs,
56
+ });
57
+ const end = participantEnd(participant.schedule, scheduledAt);
64
58
  return {
65
- ...participantWindow(participant, {
66
- sessionBudgetMs: deps.timeoutMs,
67
- sandboxMs: deps.sandboxMs,
68
- }),
69
- ...(end === undefined ? {} : { scheduledEnd: end }),
59
+ timeoutMs: own.sessionMs,
60
+ sandboxMs: own.sandboxMs,
61
+ ...(end === undefined ? {} : { end }),
70
62
  };
71
63
  }
72
64
  /**
@@ -74,33 +66,17 @@ export function scheduledParticipantDeps(participant, deps, scheduledAt) {
74
66
  * declared participant runs at once, because a throttle nobody asked for silently turns "N actors
75
67
  * live" into waves; total session count and spend are the same either way, only wall-clock
76
68
  * and simultaneity differ. A declared execution.concurrency is a cap, clamped to [1, participants].
77
- * The planner records it; the route may only lower it from the environment.
69
+ * participantsAtOnce then lowers it to what the E2B plan and HUMANISH_CUA_MAX_CONCURRENCY allow.
78
70
  */
79
71
  export function boundedConcurrency(declared, participantCount) {
80
72
  return Math.max(1, declared === undefined ? participantCount : Math.min(Math.max(1, declared), participantCount));
81
73
  }
82
- /**
83
- * The planned bound, lowered by the env override. The override may only lower it (never raise
84
- * concurrent paid desktops), and a lowering is reported via envLoweredFrom so the
85
- * plan never silently disagrees with the manifest.
86
- */
87
- function envLoweredConcurrency(planned, participantCount, env) {
88
- const envLower = readPositiveInt(env[CUA_MAX_CONCURRENCY_ENV], 0);
89
- if (envLower > 0 && envLower < planned) {
90
- return {
91
- bound: Math.max(1, Math.min(planned, envLower, participantCount)),
92
- envLoweredFrom: planned,
93
- };
94
- }
95
- return { bound: planned };
96
- }
97
74
  /**
98
75
  * Build the participant runs and the public plan from a computer-use plan (pure). The same plan
99
- * appears in dry-run and live, marked $0 for a dry run. `env` may lower the planned concurrency;
100
- * `personas` are the compiled committed personas the prompts carry.
76
+ * appears in dry-run and live, marked $0 for a dry run. `personas` are the compiled committed
77
+ * personas the prompts carry.
101
78
  */
102
79
  export function participantRunsAndPlan(plan, opts = {}) {
103
- const env = opts.env ?? {};
104
80
  // Who each participant is (id, persona, focus, device, limits) comes from the plan; this adds
105
81
  // what the route derives from it: the prompt, bundle ids and artifact paths.
106
82
  const { participants } = plan.runner;
@@ -136,58 +112,54 @@ export function participantRunsAndPlan(plan, opts = {}) {
136
112
  traceArtifactPath: participantCount === 1 ? "actor.json" : `actors/${run.streamId}.json`,
137
113
  };
138
114
  });
139
- const resolved = envLoweredConcurrency(plan.concurrency, participantCount, env);
140
- const concurrency = resolved.bound;
141
- const { sessionBudgetMs } = plan;
142
- const schedule = resolved.envLoweredFrom === undefined
143
- ? plan.arrivals
144
- : planArrivals(participants.map((participant) => participant.startAfterMs), { slots: concurrency, sessionMs: sessionBudgetMs }, participants.map((participant) => participantSessionMs(participant, sessionBudgetMs)));
145
- const sandboxesMs = participants.reduce((sum, participant) => sum + participantWindow(participant, plan).sandboxMs, 0);
115
+ const { concurrency, sessionBudgetMs, arrivals: schedule, envLoweredConcurrencyFrom } = plan;
116
+ const study = { sessionMs: sessionBudgetMs, sandboxMs: plan.sandboxMs };
117
+ const sandboxesMs = participants.reduce((sum, participant) => sum + participantSession(participant.schedule, study).sandboxMs, 0);
146
118
  const participantPlan = {
147
119
  strategy: CUA_FANOUT_STRATEGY,
148
120
  laneCount: participantCount,
149
121
  concurrency,
150
- ...(resolved.envLoweredFrom === undefined
151
- ? {}
152
- : { envLoweredConcurrencyFrom: resolved.envLoweredFrom }),
122
+ ...(envLoweredConcurrencyFrom === undefined ? {} : { envLoweredConcurrencyFrom }),
153
123
  waves: Math.ceil(participantCount / concurrency),
154
124
  perLaneSessionBudgetMs: sessionBudgetMs,
155
125
  worstCaseSandboxMinutes: Math.round(sandboxesMs / 60_000),
156
126
  ...(schedule.declared ? { schedule } : {}),
157
127
  dryRun: plan.dryRun,
158
- lanes: runs.map((spec) => ({
159
- id: spec.planned.id,
160
- ...(spec.planned.labels.actorType === undefined
161
- ? {}
162
- : { actorType: spec.planned.labels.actorType }),
163
- ...(spec.planned.labels.surface === undefined
164
- ? {}
165
- : { surface: spec.planned.labels.surface }),
166
- ...(spec.planned.labels.caseGroup === undefined
167
- ? {}
168
- : { caseGroup: spec.planned.labels.caseGroup }),
169
- index: spec.planned.index + 1,
170
- persona: spec.persona.id,
171
- device: spec.planned.device.name,
172
- resolution: spec.planned.device.resolution,
173
- instructionDigest: spec.persona.promptDigest,
174
- ...(spec.planned.limits.reasoningEffort === undefined
175
- ? {}
176
- : { reasoningEffort: spec.planned.limits.reasoningEffort }),
177
- ...(spec.planned.limits.maxOutputTokens === undefined
178
- ? {}
179
- : { maxOutputTokens: spec.planned.limits.maxOutputTokens }),
180
- ...(spec.planned.targetUrl === undefined
181
- ? {}
182
- : { targetDigest: digestUrl(spec.planned.targetUrl) }),
183
- ...(spec.planned.startAfterMs === undefined
184
- ? {}
185
- : { startAfterMs: spec.planned.startAfterMs }),
186
- ...(spec.planned.endAfterMs === undefined ? {} : { endAfterMs: spec.planned.endAfterMs }),
187
- })),
128
+ lanes: runs.map((spec) => planEntry(spec)),
188
129
  };
189
130
  return { runs, participantPlan };
190
131
  }
132
+ /** One participant's row in the plan: ids, labels, device, digests and its declared times. */
133
+ function planEntry(spec) {
134
+ const { startAfterMs, end } = spec.planned.schedule;
135
+ return {
136
+ id: spec.planned.id,
137
+ ...(spec.planned.labels.actorType === undefined
138
+ ? {}
139
+ : { actorType: spec.planned.labels.actorType }),
140
+ ...(spec.planned.labels.surface === undefined ? {} : { surface: spec.planned.labels.surface }),
141
+ ...(spec.planned.labels.caseGroup === undefined
142
+ ? {}
143
+ : { caseGroup: spec.planned.labels.caseGroup }),
144
+ index: spec.planned.index + 1,
145
+ persona: spec.persona.id,
146
+ device: spec.planned.device.name,
147
+ resolution: spec.planned.device.resolution,
148
+ instructionDigest: spec.persona.promptDigest,
149
+ ...(spec.planned.limits.reasoningEffort === undefined
150
+ ? {}
151
+ : { reasoningEffort: spec.planned.limits.reasoningEffort }),
152
+ ...(spec.planned.limits.maxOutputTokens === undefined
153
+ ? {}
154
+ : { maxOutputTokens: spec.planned.limits.maxOutputTokens }),
155
+ ...(spec.planned.targetUrl === undefined
156
+ ? {}
157
+ : { targetDigest: digestUrl(spec.planned.targetUrl) }),
158
+ ...(startAfterMs === undefined ? {} : { startAfterMs }),
159
+ ...(end?.kind === "scheduled_end" ? { endAfterMs: end.endAfterMs } : {}),
160
+ ...(end?.kind === "session_cap" ? { maxSessionMs: end.windowMs } : {}),
161
+ };
162
+ }
191
163
  /** Print the participant plan to stderr before any sandbox/provider call (public-safe: ids, devices,
192
164
  * digests, and budgets only; no prompt text, no secrets). */
193
165
  export function emitPreflightPlan(participantPlan, studyId) {
@@ -209,6 +181,7 @@ export function formatParticipantPlanEntry(entry) {
209
181
  entry.reasoningEffort ? `effort=${entry.reasoningEffort}` : undefined,
210
182
  entry.startAfterMs === undefined ? undefined : `start=+${formatDuration(entry.startAfterMs)}`,
211
183
  entry.endAfterMs === undefined ? undefined : `end=+${formatDuration(entry.endAfterMs)}`,
184
+ entry.maxSessionMs === undefined ? undefined : `cap=${formatDuration(entry.maxSessionMs)}`,
212
185
  ].filter((part) => part !== undefined);
213
186
  return `${entry.id}: persona=${entry.persona}${taxonomy.length > 0 ? ` ${taxonomy.join(" ")}` : ""} device=${entry.device} ${entry.resolution[0]}x${entry.resolution[1]} prompt#${entry.instructionDigest}${entry.targetDigest ? ` target#${entry.targetDigest}` : ""}`;
214
187
  }
@@ -236,10 +209,7 @@ export async function loadCuaParticipants(args) {
236
209
  // Compile any committed personas before planning, so the plan builder stays pure and each participant's
237
210
  // prompt carries real behavioral directives rather than a bare `Persona: <id>.` label.
238
211
  const personas = await compileParticipantPersonas(args.projectRoot, participants.map((participant) => participant.personaId));
239
- const { runs: participantRuns, participantPlan } = participantRunsAndPlan(plan, {
240
- env: args.env,
241
- personas,
242
- });
212
+ const { runs: participantRuns, participantPlan } = participantRunsAndPlan(plan, { personas });
243
213
  const rerun = plan.rerun;
244
214
  if (!rerun)
245
215
  return { ok: true, participantRuns, participantPlan };
@@ -50,4 +50,6 @@ export declare function planComputerUseStudy(config: StudyConfig, input: {
50
50
  readonly sandboxCeiling: SandboxCeiling;
51
51
  /** How many sandboxes the operator's E2B plan runs at once (concurrentSandboxes). */
52
52
  readonly concurrentSandboxes: ConcurrentSandboxes;
53
+ /** HUMANISH_CUA_MAX_CONCURRENCY (concurrencyCap), which may only lower the participants at once. */
54
+ readonly concurrencyCap?: number | undefined;
53
55
  }): ComputerUsePlanResult;
@@ -2,13 +2,14 @@
2
2
  // in its order and with its codes, then the plan the run uses. What stays in the route reads
3
3
  // external state: committed personas, a rerun's source run, keys, the local agent, subject env
4
4
  // values, spend-cap prices and the comms catch.
5
+ import { DESKTOP_FILES_ROUTE_REASON } from "../../study/desktop-files.js";
5
6
  import { resolveAutomaticAnalysis } from "../../analysis/automatic-config.js";
6
7
  import { actorRegistry, isCuaActorDescriptor, } from "../../actors/registry.js";
7
8
  import { isHttpUrl, isLoopbackUrl } from "../../study/parse/subject.js";
8
9
  import { studyUrlCredentialReason } from "../../study/url-credentials.js";
9
10
  import { subjectStateInvalidReason } from "../../study/parse/subject-state.js";
10
11
  import { brainOf, callerBrainOf, planCaps, desktopRequirements, planBase, provisionedSubject, } from "../../study/plan-base.js";
11
- import { computerUseParticipants, declaredTargets } from "../../study/plan-participants.js";
12
+ import { computerUseParticipants, declaredTargets, } from "../../study/plan-participants.js";
12
13
  import { planStarts, windowDeadlineRefusal } from "../../study/arrivals.js";
13
14
  import { computerUseParticipantCount, participantCountReason } from "../../study/routing.js";
14
15
  import { cloneTargetValidationReason, computerUseValidationReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, taskProtocolValidationReason, waitLimitValidationReason, } from "../../study/validation.js";
@@ -197,36 +198,82 @@ function rosterShapeReason(config, ceilingMs) {
197
198
  * How many of the runner's participants run at once, and when each one starts, with the session
198
199
  * budget the `ceilingMs` sandbox ceiling leaves. Hosted desktops share the E2B plan's concurrent
199
200
  * sandboxes; the local VM has its own capacity rules and the in-process route runs one
200
- * participant.
201
+ * participant. HUMANISH_CUA_MAX_CONCURRENCY (`concurrencyCap`) lowers the count on every desktop.
202
+ * `starts` holds the plan's fields for them.
201
203
  */
202
- function startsOf(config, runner, limit, ceilingMs) {
204
+ function startsOf(config, runner, limits, ceilingMs) {
203
205
  const declared = config.execution?.concurrency;
204
206
  const n = runner.participants.length;
205
- const concurrency = boundedConcurrency(declared, n);
206
- const atOnce = runner.desktop === "e2b-desktop"
207
- ? participantsAtOnce({ concurrency, declared: declared !== undefined, participants: n }, limit)
208
- : { ok: true, count: concurrency };
207
+ const hosted = runner.desktop === "e2b-desktop";
208
+ const atOnce = participantsAtOnce({
209
+ concurrency: boundedConcurrency(declared, n),
210
+ declared: declared !== undefined,
211
+ participants: n,
212
+ }, {
213
+ ...(hosted ? { sandboxes: limits.concurrentSandboxes } : {}),
214
+ cap: limits.concurrencyCap,
215
+ });
209
216
  if (!atOnce.ok)
210
217
  return atOnce;
211
218
  const sessionMs = config.execution?.timeoutMs ?? defaultSessionTimeoutMs(config, ceilingMs);
212
219
  // A participant with a declared end gets a sandbox sized from its own window.
213
- if (runner.desktop === "e2b-desktop") {
214
- const headroomMs = resolveParticipantSandboxMs(config, ceilingMs) - sessionMs;
215
- const window = windowDeadlineRefusal(runner.participants, { sessionMs, headroomMs }, ceilingMs);
220
+ if (hosted) {
221
+ const sandboxMs = resolveParticipantSandboxMs(config, ceilingMs);
222
+ const window = windowDeadlineRefusal(runner.participants, { sessionMs, sandboxMs }, ceilingMs);
216
223
  if (window !== undefined)
217
224
  return { ok: false, message: window };
218
225
  }
219
- const slots = { slots: atOnce.count, sessionMs };
226
+ const { arrivals, warnings } = planStarts(runner.participants, { slots: atOnce.count, sessionMs }, atOnce.lowered);
227
+ const lowered = atOnce.loweredFrom;
220
228
  return {
221
229
  ok: true,
222
- count: atOnce.count,
223
- ...planStarts(runner.participants, slots, atOnce.lowered),
230
+ starts: {
231
+ concurrency: atOnce.count,
232
+ ...(lowered === undefined ? {} : { envLoweredConcurrencyFrom: lowered }),
233
+ arrivals,
234
+ sessionBudgetMs: sessionMs,
235
+ },
236
+ warnings,
224
237
  };
225
238
  }
226
239
  /** The plan's rerun: the source run and, when given, the participants to rerun. */
227
240
  function rerunPlan({ sourceRunId, participantIds, }) {
228
241
  return { sourceRunId, ...(participantIds === undefined ? {} : { participantIds }) };
229
242
  }
243
+ /**
244
+ * Why participants[].files cannot run on this runner, or undefined. The parser refuses them off the
245
+ * hosted desktop; a library caller's config skips the parser, and only the hosted desktop copies
246
+ * files.
247
+ */
248
+ function desktopFilesRunnerReason(desktop, participants) {
249
+ if (desktop === "e2b-desktop" || participants.every((entry) => entry.files === undefined))
250
+ return undefined;
251
+ return `participants[].files: ${DESKTOP_FILES_ROUTE_REASON}. This study runs on a ${desktop} desktop; remove the files or set execution.target: e2b-desktop.`;
252
+ }
253
+ /**
254
+ * Why a participant's time zone cannot run on this runner, or undefined. The zone also sets the
255
+ * zone its browser runs in, which humanish sets on a hosted desktop's browser launch only: the
256
+ * local runtime's guest browser and an in-process caller's browser keep their own.
257
+ */
258
+ function timeZoneRunnerReason(desktop, participants) {
259
+ const zoned = participants.find((entry) => entry.limits.timeZone !== undefined);
260
+ if (desktop === "e2b-desktop" || zoned === undefined)
261
+ return undefined;
262
+ const declared = `Participant ${zoned.id} has the time zone ${zoned.limits.timeZone}, which sets the zone its browser runs in, and humanish sets a browser's zone only on a hosted desktop.`;
263
+ return desktop === "local-vm"
264
+ ? `${declared} This study runs on the local browser runtime. Remove actor.clock and participants[].clock, or set execution.target: e2b-desktop.`
265
+ : `${declared} This study drives your own browser through RunStudyOptions.inProcess. Remove actor.clock and participants[].clock, and set the zone in that browser yourself, or run on a hosted desktop.`;
266
+ }
267
+ /** What the participants declare that this runner cannot do, with its code, or undefined. */
268
+ function runnerRefusal(desktop, participants) {
269
+ const files = desktopFilesRunnerReason(desktop, participants);
270
+ if (files)
271
+ return { code: "HUMANISH_COMPUTER_USE_DESKTOP_FILES_INVALID", message: files };
272
+ const timeZone = timeZoneRunnerReason(desktop, participants);
273
+ if (timeZone)
274
+ return { code: "HUMANISH_COMPUTER_USE_TIME_ZONE_UNSUPPORTED", message: timeZone };
275
+ return undefined;
276
+ }
230
277
  /**
231
278
  * Plan a computer-use study. It is called for any config handed to the computer-use runner, not only
232
279
  * one routeOf sends here, so a config for another route gets this route's refusal.
@@ -311,7 +358,10 @@ export function planComputerUseStudy(config, input) {
311
358
  : isLocalBrowserStudy(config)
312
359
  ? { desktop: "local-vm", brain, participants: [first, ...rest], subject: appUrlSubject }
313
360
  : { desktop: "e2b-desktop", brain, participants: [first, ...rest], subject: hosted };
314
- const starts = startsOf(config, runner, input.concurrentSandboxes, ceiling.ms);
361
+ const unsupported = runnerRefusal(runner.desktop, participants);
362
+ if (unsupported)
363
+ return refuse("in-scope", unsupported.code, unsupported.message, actor);
364
+ const starts = startsOf(config, runner, input, ceiling.ms);
315
365
  if (!starts.ok)
316
366
  return refuse("after-personas", "HUMANISH_COMPUTER_USE_FANOUT_INVALID", starts.message, actor);
317
367
  const provisioned = source === "clone" || source === "local-tree";
@@ -324,9 +374,7 @@ export function planComputerUseStudy(config, input) {
324
374
  route: "computer-use",
325
375
  actor,
326
376
  runner,
327
- concurrency: starts.count,
328
- arrivals: starts.arrivals,
329
- sessionBudgetMs: starts.arrivals.sessionMs,
377
+ ...starts.starts,
330
378
  sandboxMs: resolveParticipantSandboxMs(config, ceiling.ms),
331
379
  caps: planCaps(config),
332
380
  ...(input.rerun === undefined ? {} : { rerun: rerunPlan(input.rerun) }),
@@ -7,6 +7,7 @@ import {} from "../../run/streams.js";
7
7
  import { plural } from "../../run/text.js";
8
8
  import { access } from "node:fs/promises";
9
9
  import { cli } from "../../cli/invocation.js";
10
+ import { startingWithTheRun } from "../../study/arrivals.js";
10
11
  export async function resolveCuaRerunSelection(args) {
11
12
  const { studyId, sourceRunId } = args;
12
13
  // Every refusal ends with the command that runs the whole study, which is what a user who
@@ -59,7 +60,7 @@ export async function resolveCuaRerunSelection(args) {
59
60
  }
60
61
  // A rerun runs the selected participants again now, all together: the study's start offsets
61
62
  // placed them in the first run's day, and that day is over.
62
- const selectedSpecs = selectedIds.map((id) => startingWithTheRun(specsById.get(id)));
63
+ const selectedSpecs = selectedIds.map((id) => startingNow(specsById.get(id)));
63
64
  const selectedPlanIds = new Set(selectedIds);
64
65
  const selectedPlanEntries = args.participantPlan.lanes
65
66
  .filter((entry) => selectedPlanIds.has(entry.id))
@@ -86,11 +87,13 @@ export async function resolveCuaRerunSelection(args) {
86
87
  },
87
88
  };
88
89
  }
89
- function startingWithTheRun(spec) {
90
- if (spec.planned.startAfterMs === undefined)
90
+ function startingNow(spec) {
91
+ if (spec.planned.schedule.startAfterMs === undefined)
91
92
  return spec;
92
- const { startAfterMs: _offset, ...planned } = spec.planned;
93
- return { ...spec, planned };
93
+ return {
94
+ ...spec,
95
+ planned: { ...spec.planned, schedule: startingWithTheRun(spec.planned.schedule) },
96
+ };
94
97
  }
95
98
  function uniqueIds(values) {
96
99
  const seen = new Set();
@@ -115,6 +115,7 @@ export declare function admitCuaRun(plan: ComputerUsePlan, input: ComputerUseRun
115
115
  publicRepo: string | undefined;
116
116
  localTreeArchive: LocalTreeArchive | undefined;
117
117
  localTreeArchiveBuffer: ArrayBuffer | undefined;
118
+ desktopFiles: ReadonlyMap<string, readonly import("../../study/desktop-files.js").DesktopFile[]>;
118
119
  };
119
120
  }>;
120
121
  /** Starts the run and builds what the participants and the finish read: the deps and the bundle base. */
@@ -24,6 +24,7 @@ import { projectParticipantSubjects } from "./subject-projection.js";
24
24
  import { participantSubjectEnv, } from "./types.js";
25
25
  import { capacityShortfall } from "../../substrates/local/capacity.js";
26
26
  import { studyPersonaIds } from "../../study/persona-resolve.js";
27
+ import { readDesktopFiles } from "../../study/desktop-files.js";
27
28
  import { refusedResult } from "../../run/study-result.js";
28
29
  /** The physical project, bound before any caller hook runs. */
29
30
  async function bindProject(cwd) {
@@ -107,7 +108,7 @@ export async function admitCuaRun(plan, input, config) {
107
108
  ? comms?.email?.external
108
109
  : undefined;
109
110
  const externalCommsEmail = externalCommsConfig ? comms?.email : undefined;
110
- const participants = await loadCuaParticipants({ plan, cwd, projectRoot, env });
111
+ const participants = await loadCuaParticipants({ plan, cwd, projectRoot });
111
112
  if (!participants.ok) {
112
113
  return refuse(participants.code, participants.message, descriptor.id);
113
114
  }
@@ -132,6 +133,9 @@ export async function admitCuaRun(plan, input, config) {
132
133
  ...subjectEnvNames.map((name) => env[name] ?? ""),
133
134
  ]);
134
135
  sanitizeParticipantRuns(participantRuns, secrets.scrub);
136
+ const desktopFiles = await readStudyDesktopFiles(input.cwd, plan, participantRuns, secrets.scrub);
137
+ if (!desktopFiles.ok)
138
+ return refuse("HUMANISH_COMPUTER_USE_DESKTOP_FILES_INVALID", desktopFiles.message, descriptor.id);
135
139
  const redactRepoLabel = plan.residual.policies?.redactRepos ?? subjectEnvNames.includes("GITHUB_TOKEN");
136
140
  const publicRepo = subject.kind === "clone" ? (redactRepoLabel ? "repo-01" : subject.repo) : undefined;
137
141
  // Key-gating is route-aware: the in-process route uses the caller's own model + executor, and
@@ -195,9 +199,32 @@ export async function admitCuaRun(plan, input, config) {
195
199
  publicRepo,
196
200
  localTreeArchive,
197
201
  localTreeArchiveBuffer,
202
+ desktopFiles: desktopFiles.files,
198
203
  },
199
204
  };
200
205
  }
206
+ /**
207
+ * Each participant's files, read on dry runs too so a bad file is named before a live run. Paths
208
+ * are relative to the folder that holds the study file; a library caller's config comes from no
209
+ * file, so its paths are relative to the project. An `as` that holds a value `scrub` removes, such
210
+ * as a key from the environment, is refused: the run records `as`, and redacting it would record a
211
+ * different path.
212
+ */
213
+ async function readStudyDesktopFiles(cwd, plan, participantRuns, scrub) {
214
+ const folder = plan.study?.path === undefined
215
+ ? path.resolve(cwd)
216
+ : path.dirname(path.resolve(cwd, plan.study.path));
217
+ const read = await readDesktopFiles(folder, participantRuns.map((run) => run.planned));
218
+ if (!read.ok)
219
+ return read;
220
+ const named = [...read.files].find(([, files]) => files.some((file) => scrub(file.as) !== file.as));
221
+ return named === undefined
222
+ ? read
223
+ : {
224
+ ok: false,
225
+ message: `Participant ${named[0]}'s files name an as path that holds a key or another value from this run's environment, which the run would record. Rename the file.`,
226
+ };
227
+ }
201
228
  /**
202
229
  * Whether a local study's desktops at once fit the local runtime. Read before any desktop: a
203
230
  * desktop past the VM's memory is killed mid-session, after its participant has spent.
@@ -317,6 +344,7 @@ function cuaParticipantDeps(plan, input, admitted, run) {
317
344
  createDesktop: input.localVm?.desktop,
318
345
  ...callerDriving(input, config),
319
346
  localTreeArchiveBuffer: admitted.localTreeArchiveBuffer,
347
+ desktopFiles: admitted.desktopFiles.size === 0 ? undefined : admitted.desktopFiles,
320
348
  externalComms: externalCommsConfig === undefined || externalCommsEmail === undefined
321
349
  ? undefined
322
350
  : { email: externalCommsEmail, inboxUrl: externalInboxUrl(externalCommsConfig) },
@@ -4,7 +4,7 @@ import type { DesktopBrowserEvidence } from "../../substrates/e2b/desktop-browse
4
4
  import type { ActorPersonaRef } from "../../actors/contract.js";
5
5
  import type { CuaLoopResult } from "../../actors/computer-use/loop.js";
6
6
  import { type BundleRun, type RunBundle, type RunFeedbackCandidate, type RunProviderResource, type RunSubjectProvenance } from "../../run/bundle.js";
7
- import { type RunDesktopGeometry, type RunStream } from "../../run/streams.js";
7
+ import { type RunDesktopFile, type RunDesktopGeometry, type RunStream } from "../../run/streams.js";
8
8
  import { type Verdict } from "../../run/judge.js";
9
9
  import { type DesktopUsage } from "../../run/cost-summary.js";
10
10
  import type { HostSuspensionReading } from "../../run/host-suspension.js";
@@ -41,6 +41,10 @@ export declare function buildSingleParticipantBundle(args: {
41
41
  /** Runtime screen/window/viewport evidence. `viewport` inside this object must be measured. */
42
42
  desktopGeometry?: RunDesktopGeometry;
43
43
  recording?: RunDesktopRecording;
44
+ /** participants[].files confirmed on the participant's desktop. */
45
+ desktopFiles?: RunDesktopFile[];
46
+ /** The participant's time zone, which its turns and its hosted browser used. */
47
+ timeZone?: string;
44
48
  /** Device-preset touch metadata echoed on the measured stream viewport (a prompt signal on
45
49
  * this route, never a rendered claim); the measured width/height/DPR stay authoritative. */
46
50
  isMobile?: boolean;
@@ -5,6 +5,7 @@ import {} from "../../run/streams.js";
5
5
  import { aggregateTaskFunnels, formatParticipantOutcomes, formatRunTaskFunnel, participantSummary, withCuaReviewProvenance, } from "../../run/outcomes.js";
6
6
  import { participantCaption } from "../../run/participant-caption.js";
7
7
  import { actorEnding } from "../../actors/stop-cause.js";
8
+ import { cuaGoalSource, ranUntilClockEnd } from "../../actors/goal-source.js";
8
9
  import { describeSubjectState, phaseEventIdSuffix, publicSafeAppUrlLabel, subjectProvenanceMessage, } from "./bundle-parts.js";
9
10
  import { participantFeedbackCandidates } from "./participant-feedback.js";
10
11
  import { judgeParticipantRecords } from "../../run/judge.js";
@@ -46,6 +47,7 @@ function runCost(args) {
46
47
  }
47
48
  /** The participant's status, reason, last frame, geometry and screenshot mode, shared by its records. */
48
49
  function participantView(args, publicAppUrl) {
50
+ const ranUntil = args.session === undefined ? undefined : ranUntilClockEnd(args.session.trace);
49
51
  const judgment = judgeParticipantRecords([
50
52
  {
51
53
  ...(args.session === undefined
@@ -55,6 +57,7 @@ function participantView(args, publicAppUrl) {
55
57
  completionReason: args.session.completionReason,
56
58
  reason: args.session.reason,
57
59
  }),
60
+ ...(ranUntil === undefined ? {} : { ranUntil }),
58
61
  ...(args.sessionError === undefined ? {} : { sessionError: args.sessionError }),
59
62
  inProgress: args.inProgress === true,
60
63
  skipped: false,
@@ -151,6 +154,8 @@ function singleStream(args, view) {
151
154
  },
152
155
  }),
153
156
  ...(desktopGeometry === undefined ? {} : { desktopGeometry }),
157
+ ...(args.desktopFiles === undefined ? {} : { desktopFiles: args.desktopFiles }),
158
+ ...(args.timeZone === undefined ? {} : { timeZone: args.timeZone }),
154
159
  ...(args.recording === undefined ? {} : { recording: args.recording }),
155
160
  ui: {
156
161
  route: publicAppUrl,
@@ -296,8 +301,13 @@ function singleSummary(args, view, facts) {
296
301
  : `1 participant was planned, but the run failed before their session finished: ${view.reason}`;
297
302
  }
298
303
  const ending = actorEnding(args.session.trace);
304
+ const goalSource = cuaGoalSource(args.session.trace);
299
305
  const outcomes = formatParticipantOutcomes(view.tally, [
300
- { status: args.session.status, ...(ending === undefined ? {} : { label: ending.label }) },
306
+ {
307
+ status: args.session.status,
308
+ ...(ending === undefined ? {} : { label: ending.label }),
309
+ ...(goalSource === undefined ? {} : { goalSource }),
310
+ },
301
311
  ]);
302
312
  return [
303
313
  `1 participant took part: ${outcomes}.`,
@@ -13,7 +13,7 @@ import { type CuaDiagnostics } from "./diagnostics.js";
13
13
  import type { ActorCompletionReason, ActorStatus, ActorStopCause, ActorTokenUsage, ActorTraceItem } from "../../actors/contract.js";
14
14
  import { type CuaActorDescriptor } from "../../actors/registry.js";
15
15
  import type { StudyDeps } from "../../study/study-deps.js";
16
- import type { ArrivalPlan, ParticipantArrival } from "../../study/arrivals.js";
16
+ import type { ArrivalPlan, ParticipantArrival, ParticipantEnd } from "../../study/arrivals.js";
17
17
  import type { StudyEvent, ParticipantRef } from "../../study/run-study-events.js";
18
18
  import type { InProcessDriver, ProviderFactory, RunStudyHomes } from "../../study/run-study-homes.js";
19
19
  import { type BrowserScorer } from "../../study/adapter-extension.js";
@@ -23,15 +23,15 @@ import { type DesktopResourceObservation } from "../../substrates/e2b/desktop-re
23
23
  import { type DetachedTimers } from "../../substrates/detached.js";
24
24
  import type { Brain, ComputerUsePlan, ComputerUseRunner, ResidualConfig } from "../../study/plan-types.js";
25
25
  import { type StudyCommsEmail } from "../../study/types.js";
26
+ import type { DesktopFile } from "../../study/desktop-files.js";
26
27
  import { type ObserverResult } from "../../observer/render.js";
27
28
  import { type BundleRun, type RunBundle, type RunRerunLineage, type RunScorerProvenance, type RunSubjectProvenance, type RunSubjectStateStepRecord } from "../../run/bundle.js";
28
- import { type RunDesktopGeometry } from "../../run/streams.js";
29
+ import { type RunDesktopFile, type RunDesktopGeometry } from "../../run/streams.js";
29
30
  import { type PreparedOutputRoot } from "../../run/contained-output.js";
30
31
  import type { ComputerUseParticipant, SharedWorldParticipant } from "../../study/plan-participants.js";
31
32
  import type { ResolvedParticipant } from "../../run/participant.js";
32
33
  import { type StudyResultErrorCode, type StudyResultIdentity } from "../../run/study-result.js";
33
34
  export declare const CUA_FANOUT_STRATEGY: "per-lane-worlds";
34
- export declare const CUA_MAX_CONCURRENCY_ENV = "HUMANISH_CUA_MAX_CONCURRENCY";
35
35
  export declare const DEFAULT_APP_URL_SESSION_TIMEOUT_MS: number;
36
36
  export declare const MIN_DERIVED_SESSION_TIMEOUT_MS: number;
37
37
  /**
@@ -114,6 +114,8 @@ export interface CuaParticipantPlanEntry {
114
114
  startAfterMs?: number;
115
115
  /** The participant's declared end, on the same clock. */
116
116
  endAfterMs?: number;
117
+ /** The participant's session cap, from the start of its session. */
118
+ maxSessionMs?: number;
117
119
  }
118
120
  /** The pre-flight spend and participant plan (pure; printed to stderr + recorded as a bundle event before
119
121
  * any sandbox or provider call; identical in dry-run, marked $0). */
@@ -123,8 +125,8 @@ export interface CuaParticipantPlan {
123
125
  /** Effective in-flight bound (defaults to laneCount, all participants live; a declared
124
126
  * execution.concurrency is a cap; the env override may only lower it). */
125
127
  concurrency: number;
126
- /** Present when the env override lowered the bound below the config's value, recorded so the
127
- * plan never silently disagrees with the manifest. */
128
+ /** Present when HUMANISH_CUA_MAX_CONCURRENCY lowered the bound (the plan's
129
+ * envLoweredConcurrencyFrom), recorded so the plan never silently disagrees with the manifest. */
128
130
  envLoweredConcurrencyFrom?: number;
129
131
  /** ceil(laneCount / concurrency). */
130
132
  waves: number;
@@ -195,7 +197,7 @@ export interface CuaParticipantSummary {
195
197
  concurrency: number;
196
198
  waves: number;
197
199
  }
198
- export type CuaActorStudyErrorCode = StudyResultErrorCode | "HUMANISH_COMPUTER_USE_FAILED" | "HUMANISH_COMPUTER_USE_KEYS_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_SIGNIN_REQUIRED" | "HUMANISH_COMPUTER_USE_SUBJECT_ENV_MISSING" | "HUMANISH_COMPUTER_USE_ACTOR_UNSUPPORTED" | "HUMANISH_COMPUTER_USE_SUBJECT_INVALID" | "HUMANISH_COMPUTER_USE_SUBJECT_UNSAFE" | "HUMANISH_COMPUTER_USE_EXECUTOR_NO_PROVIDER" | "HUMANISH_COMPUTER_USE_LOCAL_APP_NO_EXECUTOR" | "HUMANISH_COMPUTER_USE_LOCAL_DESKTOP_MISSING" | "HUMANISH_COMPUTER_USE_LOCAL_CAPACITY_EXCEEDED" | "HUMANISH_COMPUTER_USE_DESKTOP_OUT_OF_MEMORY" | "HUMANISH_COMPUTER_USE_FANOUT_INVALID" | "HUMANISH_COMPUTER_USE_RERUN_INVALID" | "HUMANISH_COMPUTER_USE_DEVICE_GEOMETRY" | "HUMANISH_COMPUTER_USE_SUBJECT_BUILD_FAILED" | "HUMANISH_COMPUTER_USE_UNPRICED_CAP" | "HUMANISH_COMPUTER_USE_COMMS_CATCH_UNREACHABLE" | "HUMANISH_COMPUTER_USE_COMMS_TOKEN_INVALID" | "HUMANISH_WATCH_ALLOW_REQUIRES_OAUTH" | "HUMANISH_WATCH_OAUTH_REQUIRES_TUNNEL" | "HUMANISH_WATCH_OPTION_CONFLICT" | "HUMANISH_WATCH_TUNNEL_REQUIRES_EXPOSE" | "HUMANISH_WATCH_EXPOSE_REQUIRES_EDGE_AUTH" | "HUMANISH_WATCH_EXPOSE_REQUIRES_LIVE_FOLLOW" | "HUMANISH_WATCH_SAFE_NOT_APPLICABLE" | "HUMANISH_SERVE_TUNNEL_NOT_FOUND" | "HUMANISH_SERVE_TUNNEL_START_FAILED";
200
+ export type CuaActorStudyErrorCode = StudyResultErrorCode | "HUMANISH_COMPUTER_USE_FAILED" | "HUMANISH_COMPUTER_USE_KEYS_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_SIGNIN_REQUIRED" | "HUMANISH_COMPUTER_USE_SUBJECT_ENV_MISSING" | "HUMANISH_COMPUTER_USE_ACTOR_UNSUPPORTED" | "HUMANISH_COMPUTER_USE_SUBJECT_INVALID" | "HUMANISH_COMPUTER_USE_SUBJECT_UNSAFE" | "HUMANISH_COMPUTER_USE_EXECUTOR_NO_PROVIDER" | "HUMANISH_COMPUTER_USE_LOCAL_APP_NO_EXECUTOR" | "HUMANISH_COMPUTER_USE_LOCAL_DESKTOP_MISSING" | "HUMANISH_COMPUTER_USE_LOCAL_CAPACITY_EXCEEDED" | "HUMANISH_COMPUTER_USE_DESKTOP_OUT_OF_MEMORY" | "HUMANISH_COMPUTER_USE_DESKTOP_FILES_INVALID" | "HUMANISH_COMPUTER_USE_TIME_ZONE_UNSUPPORTED" | "HUMANISH_COMPUTER_USE_FANOUT_INVALID" | "HUMANISH_COMPUTER_USE_RERUN_INVALID" | "HUMANISH_COMPUTER_USE_DEVICE_GEOMETRY" | "HUMANISH_COMPUTER_USE_SUBJECT_BUILD_FAILED" | "HUMANISH_COMPUTER_USE_UNPRICED_CAP" | "HUMANISH_COMPUTER_USE_COMMS_CATCH_UNREACHABLE" | "HUMANISH_COMPUTER_USE_COMMS_TOKEN_INVALID" | "HUMANISH_WATCH_ALLOW_REQUIRES_OAUTH" | "HUMANISH_WATCH_OAUTH_REQUIRES_TUNNEL" | "HUMANISH_WATCH_OPTION_CONFLICT" | "HUMANISH_WATCH_TUNNEL_REQUIRES_EXPOSE" | "HUMANISH_WATCH_EXPOSE_REQUIRES_EDGE_AUTH" | "HUMANISH_WATCH_EXPOSE_REQUIRES_LIVE_FOLLOW" | "HUMANISH_WATCH_SAFE_NOT_APPLICABLE" | "HUMANISH_SERVE_TUNNEL_NOT_FOUND" | "HUMANISH_SERVE_TUNNEL_START_FAILED";
199
201
  export interface CuaActorStudyResult extends AutomaticAnalysisResult, StudyResultIdentity<"computer-use"> {
200
202
  /** True when the Observer verified the bundle, all live participants passed credibility checks
201
203
  * (or this is a dry-run), and no declared adapter/scorer verdict failed. */
@@ -316,6 +318,9 @@ export interface E2BDesktopDeps {
316
318
  /** Local-tree route only: the once-per-run packed archive bytes, shared byte-identically
317
319
  * across every fan-out participant's upload step. Absent on dry-run and every other route. */
318
320
  localTreeArchiveBuffer?: ArrayBuffer;
321
+ /** Computer use on a hosted desktop: each participant's files (participants[].files) by id,
322
+ * read on this machine before any sandbox. A participant without an entry receives none. */
323
+ desktopFiles?: ReadonlyMap<string, readonly DesktopFile[]>;
319
324
  env: Record<string, string | undefined>;
320
325
  e2bApiKey: string;
321
326
  requestTimeoutMs: number;
@@ -367,14 +372,12 @@ export interface ParticipantModelDeps {
367
372
  openaiApiKey: string;
368
373
  timeoutMs: number;
369
374
  /**
370
- * The participant's scheduled end on `now`'s clock, when it declares participants[].endAfterMs
371
- * (scheduledParticipantDeps). Its session ends there with stop cause `scheduled_end`.
375
+ * How the participant's session ends, when it declares participants[].endAfterMs or maxSessionMs
376
+ * (scheduledParticipantDeps): at its scheduled end on `now`'s clock, with stop cause
377
+ * `scheduled_end`, or at its cap after the session starts, with stop cause `session_cap`.
372
378
  */
373
- scheduledEnd?: {
374
- readonly atMs: number;
375
- readonly windDownMs: number;
376
- };
377
- /** The run's clock. A participant with a scheduled end runs its session on it. */
379
+ end?: ParticipantEnd;
380
+ /** The run's clock. A participant with an end or a cap runs its session on it. */
378
381
  now: () => number;
379
382
  participantCount: number;
380
383
  artifactRoot: PreparedOutputRoot;
@@ -449,6 +452,8 @@ export interface ParticipantRunOutcome {
449
452
  /** Requested + measured desktop/browser geometry. Viewport is absent when measurement failed. */
450
453
  desktopGeometry?: RunDesktopGeometry;
451
454
  recording?: RunDesktopRecording;
455
+ /** participants[].files confirmed on the participant's desktop. */
456
+ desktopFiles?: RunDesktopFile[];
452
457
  stateStepRecords: RunSubjectStateStepRecord[];
453
458
  /** Completed subject-phase records (clone/upload/extract/install/build/ready/state groups),
454
459
  * folded into bundle.events at build time. Empty on the in-process route (no provisioning). */
@@ -15,9 +15,6 @@ import {} from "../../run/study-result.js";
15
15
  // The fan-out topology of this route: N participants = N independent E2B desktop sandboxes,
16
16
  // each its own world (clone/serve + subject.state per participant). Shared-world has its own route.
17
17
  export const CUA_FANOUT_STRATEGY = "per-lane-worlds";
18
- // Env override that may only lower the effective concurrency (never raise concurrent paid
19
- // desktops). Read names-only into a local; the value never persists.
20
- export const CUA_MAX_CONCURRENCY_ENV = "HUMANISH_CUA_MAX_CONCURRENCY";
21
18
  // The default session budget, sized so a study can finish (docs/principles/three-roles.md: a
22
19
  // session ends because the participant is done, not because a timer fired; the time-box is a
23
20
  // session-level cap a researcher sets generously; spend protection is the dollar caps' job).