humanish 0.115.0 → 0.117.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 (195) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +7 -6
  3. package/TELEMETRY.md +6 -0
  4. package/dist/actors/codex/app-server-ui.js +11 -4
  5. package/dist/actors/codex/restricted-participant-policy.d.ts +0 -2
  6. package/dist/actors/codex/restricted-participant-policy.js +4 -32
  7. package/dist/actors/codex/restricted-participant.d.ts +5 -0
  8. package/dist/actors/codex/restricted-participant.js +3 -2
  9. package/dist/actors/computer-use/actor.d.ts +4 -0
  10. package/dist/actors/computer-use/actor.js +2 -0
  11. package/dist/actors/computer-use/loop/actions.d.ts +4 -4
  12. package/dist/actors/computer-use/loop/actions.js +25 -18
  13. package/dist/actors/computer-use/loop/debrief.js +61 -27
  14. package/dist/actors/computer-use/loop/session.d.ts +3 -0
  15. package/dist/actors/computer-use/loop/session.js +9 -1
  16. package/dist/actors/computer-use/loop/trace.js +1 -0
  17. package/dist/actors/computer-use/loop/types.d.ts +16 -14
  18. package/dist/actors/computer-use/loop.d.ts +1 -1
  19. package/dist/actors/computer-use/loop.js +1 -1
  20. package/dist/actors/computer-use/wait.d.ts +74 -0
  21. package/dist/actors/computer-use/wait.js +100 -0
  22. package/dist/actors/contract.d.ts +14 -0
  23. package/dist/actors/scripted-browser/steps.js +6 -5
  24. package/dist/analysis/analysis-limits.d.ts +35 -0
  25. package/dist/analysis/analysis-limits.js +41 -0
  26. package/dist/analysis/validation.js +14 -21
  27. package/dist/browser-control/protocol.d.ts +86 -6
  28. package/dist/browser-control/protocol.js +13 -6
  29. package/dist/cli/commands/analyze.js +2 -1
  30. package/dist/cli/commands/notes.js +2 -1
  31. package/dist/cli/commands/run-signals.js +5 -1
  32. package/dist/cli/commands/runs.js +2 -1
  33. package/dist/cli/commands/setup.js +3 -3
  34. package/dist/cli/commands/study-format.d.ts +5 -2
  35. package/dist/cli/commands/study-format.js +29 -9
  36. package/dist/cli/commands/study-route-computer-use.js +2 -2
  37. package/dist/cli/commands/study-route-scripted.js +1 -1
  38. package/dist/cli/commands/tui.js +59 -17
  39. package/dist/cli/doctor.js +9 -2
  40. package/dist/cli/findings-text.d.ts +5 -0
  41. package/dist/cli/findings-text.js +169 -0
  42. package/dist/cli/findings.d.ts +0 -4
  43. package/dist/cli/findings.js +5 -167
  44. package/dist/cli/io.js +6 -1
  45. package/dist/cli/update-check.js +4 -1
  46. package/dist/comms/connections.js +4 -3
  47. package/dist/comms/setup.js +18 -12
  48. package/dist/evidence/redaction.js +9 -1
  49. package/dist/evidence/secret-scrub.d.ts +15 -5
  50. package/dist/evidence/secret-scrub.js +67 -9
  51. package/dist/feedback/draft.js +1 -1
  52. package/dist/feedback/export-bundle.js +18 -14
  53. package/dist/feedback/feedback.js +25 -7
  54. package/dist/guest/desktop-executor.js +3 -1
  55. package/dist/guest/desktop-keys.js +59 -5
  56. package/dist/keys/key-resolution.d.ts +9 -0
  57. package/dist/keys/key-resolution.js +60 -48
  58. package/dist/keys/key-status.d.ts +2 -0
  59. package/dist/keys/key-status.js +9 -16
  60. package/dist/observer/pinned-files.js +1 -1
  61. package/dist/observer/render.js +3 -2
  62. package/dist/observer-app.html +5 -5
  63. package/dist/routes/computer-use/bundle-parts.d.ts +4 -4
  64. package/dist/routes/computer-use/bundle-parts.js +2 -2
  65. package/dist/routes/computer-use/bundle.d.ts +4 -5
  66. package/dist/routes/computer-use/bundle.js +1 -3
  67. package/dist/routes/computer-use/e2b-desktop/prepare.js +40 -31
  68. package/dist/routes/computer-use/fanout-records.js +10 -11
  69. package/dist/routes/computer-use/live-phase.d.ts +1 -1
  70. package/dist/routes/computer-use/live-phase.js +0 -1
  71. package/dist/routes/computer-use/local-vm.js +5 -2
  72. package/dist/routes/computer-use/participant-execution.d.ts +3 -2
  73. package/dist/routes/computer-use/participant-model.js +9 -0
  74. package/dist/routes/computer-use/participant-runs.d.ts +6 -2
  75. package/dist/routes/computer-use/participant-runs.js +9 -5
  76. package/dist/routes/computer-use/plan.d.ts +3 -0
  77. package/dist/routes/computer-use/plan.js +18 -13
  78. package/dist/routes/computer-use/rerun-selection.js +2 -2
  79. package/dist/routes/computer-use/result.js +3 -6
  80. package/dist/routes/computer-use/setup.d.ts +4 -6
  81. package/dist/routes/computer-use/setup.js +2 -6
  82. package/dist/routes/computer-use/single-bundle.d.ts +4 -5
  83. package/dist/routes/computer-use/single-bundle.js +22 -25
  84. package/dist/routes/computer-use/subject-projection.d.ts +4 -8
  85. package/dist/routes/computer-use/subject-projection.js +9 -31
  86. package/dist/routes/computer-use/types.d.ts +5 -42
  87. package/dist/routes/scripted/scenario.js +28 -12
  88. package/dist/routes/scripted/session-result.js +6 -3
  89. package/dist/routes/shared-world/bundle-records.js +2 -8
  90. package/dist/routes/shared-world/participant-specs.d.ts +0 -2
  91. package/dist/routes/shared-world/participant-specs.js +1 -20
  92. package/dist/routes/shared-world/plan.d.ts +3 -0
  93. package/dist/routes/shared-world/plan.js +25 -4
  94. package/dist/routes/shared-world/setup.js +2 -2
  95. package/dist/routes/terminal/ledger.d.ts +9 -9
  96. package/dist/routes/terminal/ledger.js +63 -40
  97. package/dist/routes/terminal/live-finish.js +7 -10
  98. package/dist/routes/terminal/live-sandbox.d.ts +1 -1
  99. package/dist/routes/terminal/live-sandbox.js +14 -11
  100. package/dist/routes/terminal/plan.d.ts +3 -0
  101. package/dist/routes/terminal/plan.js +8 -5
  102. package/dist/routes/terminal/sandbox.d.ts +4 -3
  103. package/dist/routes/terminal/sandbox.js +5 -5
  104. package/dist/routes/terminal/token-usage.d.ts +1 -2
  105. package/dist/routes/terminal/token-usage.js +4 -6
  106. package/dist/routes/terminal/types.d.ts +9 -1
  107. package/dist/run/bundle-shape.js +6 -1
  108. package/dist/run/bundle.d.ts +5 -3
  109. package/dist/run/contained-output.d.ts +53 -5
  110. package/dist/run/contained-output.js +141 -42
  111. package/dist/run/detail.d.ts +2 -1
  112. package/dist/run/detail.js +12 -7
  113. package/dist/run/escape-sequences.d.ts +11 -0
  114. package/dist/run/escape-sequences.js +93 -0
  115. package/dist/run/evidence-files.d.ts +8 -8
  116. package/dist/run/evidence-files.js +19 -23
  117. package/dist/run/locate.d.ts +32 -3
  118. package/dist/run/locate.js +45 -40
  119. package/dist/run/notes.d.ts +0 -8
  120. package/dist/run/notes.js +5 -51
  121. package/dist/run/outcomes.d.ts +24 -0
  122. package/dist/run/outcomes.js +19 -0
  123. package/dist/run/paths.d.ts +2 -0
  124. package/dist/run/paths.js +5 -1
  125. package/dist/run/preflight-receipts.js +13 -8
  126. package/dist/run/project.js +7 -4
  127. package/dist/run/reclaim-outcomes.d.ts +8 -6
  128. package/dist/run/reclaim-outcomes.js +10 -6
  129. package/dist/run/reclaim.d.ts +5 -0
  130. package/dist/run/reclaim.js +43 -16
  131. package/dist/run/run-clock.d.ts +50 -0
  132. package/dist/run/run-clock.js +66 -0
  133. package/dist/run/run.js +5 -2
  134. package/dist/run/sandbox-ids.d.ts +7 -2
  135. package/dist/run/sandbox-ids.js +27 -11
  136. package/dist/run/secrets.d.ts +2 -1
  137. package/dist/run/secrets.js +18 -22
  138. package/dist/run/stored-runs.js +14 -7
  139. package/dist/run/transient-comms-secrets.js +6 -17
  140. package/dist/run-study.js +22 -13
  141. package/dist/study/adapter-scorer-loader.js +6 -4
  142. package/dist/study/config.js +4 -1
  143. package/dist/study/discover.js +9 -6
  144. package/dist/study/init-paths.js +7 -5
  145. package/dist/study/keys.d.ts +2 -0
  146. package/dist/study/keys.js +2 -0
  147. package/dist/study/migrate/v2.js +2 -1
  148. package/dist/study/parse/actors.js +8 -0
  149. package/dist/study/persona-resolve.d.ts +2 -2
  150. package/dist/study/persona-resolve.js +9 -5
  151. package/dist/study/plan-participants.d.ts +4 -0
  152. package/dist/study/plan-participants.js +4 -0
  153. package/dist/study/plan-types.d.ts +2 -2
  154. package/dist/study/plan.d.ts +5 -3
  155. package/dist/study/plan.js +18 -9
  156. package/dist/study/preflight-probes.js +11 -3
  157. package/dist/study/preflight.js +9 -5
  158. package/dist/study/summary.d.ts +8 -2
  159. package/dist/study/summary.js +20 -22
  160. package/dist/study/types.d.ts +14 -0
  161. package/dist/study/validation.d.ts +5 -0
  162. package/dist/study/validation.js +8 -0
  163. package/dist/study/warnings.d.ts +19 -12
  164. package/dist/study/warnings.js +50 -17
  165. package/dist/subject/serve.d.ts +3 -0
  166. package/dist/subject/serve.js +4 -1
  167. package/dist/substrates/detached.js +14 -7
  168. package/dist/substrates/e2b/connection.d.ts +12 -0
  169. package/dist/substrates/e2b/connection.js +35 -0
  170. package/dist/substrates/e2b/desktop-executor.d.ts +1 -5
  171. package/dist/substrates/e2b/desktop-executor.js +45 -12
  172. package/dist/substrates/e2b/lifetime.d.ts +24 -4
  173. package/dist/substrates/e2b/lifetime.js +41 -4
  174. package/dist/substrates/e2b/sandbox.d.ts +4 -3
  175. package/dist/substrates/e2b/sandbox.js +13 -18
  176. package/dist/substrates/e2b/sdk.d.ts +20 -9
  177. package/dist/substrates/local/capacity.d.ts +18 -2
  178. package/dist/substrates/local/capacity.js +21 -2
  179. package/dist/substrates/local/runtime-host.js +21 -14
  180. package/dist/substrates/local/runtime.js +3 -11
  181. package/dist/tui/contract.d.ts +30 -5
  182. package/dist/tui/contract.js +4 -1
  183. package/dist/tui-app.js +150 -147
  184. package/dist/verify/artifacts.d.ts +9 -2
  185. package/dist/verify/artifacts.js +37 -13
  186. package/dist/verify/liveness.js +19 -5
  187. package/dist/verify/verify.js +54 -27
  188. package/docs/architecture/browser-control.md +10 -4
  189. package/docs/architecture/guest-desktop.md +7 -2
  190. package/docs/architecture/state-driven-executor.md +4 -2
  191. package/docs/contracts/run-bundle.md +18 -10
  192. package/docs/contracts/schemas.md +46 -29
  193. package/docs/contracts/study-analysis.md +10 -1
  194. package/docs/product/automatic-analysis.md +7 -1
  195. package/package.json +2 -2
@@ -2,7 +2,7 @@ import { participantAssignment } from "../../study/participant-assignment.js";
2
2
  import { redactText } from "../../evidence/redaction.js";
3
3
  import { REVIEW_SCHEMA, bundleArtifacts, bundleHead, } from "../../run/bundle.js";
4
4
  import {} from "../../run/streams.js";
5
- import { aggregateTaskFunnels, formatParticipantOutcomes, formatRunTaskFunnel, sessionEndingInWords, withCuaReviewProvenance, } from "../../run/outcomes.js";
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
8
  import { describeSubjectState, phaseEventIdSuffix, publicSafeAppUrlLabel, subjectProvenanceMessage, } from "./bundle-parts.js";
@@ -103,13 +103,11 @@ function singleSimulation(args, view) {
103
103
  mode: "browser-sim",
104
104
  progress: args.inProgress === true ? 20 : 100,
105
105
  currentStep: reason,
106
- summary: args.session
107
- ? `${name} used the app ${browserPlace(args)}, and ${sessionEndingInWords(args.session.trace)}.`
108
- : args.inProgress === true
109
- ? `${name} is using the app ${browserPlace(args)}.`
110
- : args.sessionError !== undefined
111
- ? `${name} did not finish a session: ${args.sessionError}`
112
- : `${name} would use ${publicAppUrl}; no session ran.`,
106
+ summary: participantSummary(name, { name: "the app", place: browserPlace(args), target: publicAppUrl }, {
107
+ trace: args.session?.trace,
108
+ sessionError: args.sessionError,
109
+ inProgress: args.inProgress === true,
110
+ }),
113
111
  startedAt: args.run.createdAt,
114
112
  updatedAt: args.run.createdAt,
115
113
  });
@@ -183,6 +181,7 @@ function singleStream(args, view) {
183
181
  }
184
182
  function singleEvents(args, view) {
185
183
  const { publicAppUrl, desktopGeometry } = view;
184
+ const { subject } = args;
186
185
  const events = [
187
186
  {
188
187
  id: "event-000-created",
@@ -191,30 +190,32 @@ function singleEvents(args, view) {
191
190
  type: "cua-lab.run.created",
192
191
  message: `Created a computer-use run for ${args.studyId} (actor ${args.actorId}).`,
193
192
  },
194
- args.subjectProvenance
193
+ subject.source === "clone" || subject.source === "local-tree"
195
194
  ? participantEvent(SINGLE, {
196
195
  id: "event-001-subject",
197
196
  at: args.run.createdAt,
198
197
  level: "info",
199
198
  type: "cua-lab.subject.provenance",
200
199
  // Claim "cloned/packed and served" only when it actually happened.
201
- message: `${subjectProvenanceMessage(args.subjectProvenance, publicAppUrl, args.dryRun, args.session !== undefined)} (subject env names: ${args.subjectProvenance.envNames.length > 0 ? args.subjectProvenance.envNames.join(", ") : "none"}; values never persisted); state: ${describeSubjectState(args.subjectProvenance.state, args.dryRun)}.`,
200
+ message: `${subjectProvenanceMessage(subject, publicAppUrl, args.dryRun, args.session !== undefined)} (subject env names: ${subject.envNames !== undefined && subject.envNames.length > 0 ? subject.envNames.join(", ") : "none"}; values never persisted); state: ${describeSubjectState(subject.state, args.dryRun)}.`,
202
201
  })
203
202
  : participantEvent(SINGLE, {
204
203
  id: "event-001-subject",
205
204
  at: args.run.createdAt,
206
205
  level: "info",
207
206
  type: "cua-lab.subject.declared",
208
- // Declare what the subject was, including the absence of a pin. A
209
- // local-app / in-process subject is an already-running local dev server the caller
210
- // provisioned; it cannot be commit-pinned, so its provenance is unpinned and
211
- // no E2B desktop was created. A plain app-url entry runs inside the desktop sandbox, or,
212
- // on the local VM, reaches the host's loopback from the VM's browser.
213
- message: args.entryKind === "local-app"
214
- ? `Subject app declared at ${publicAppUrl}: a running local dev server driven in this process, with no clone and no E2B desktop. Provenance: provided by the caller and unpinned, because a running dev server has no commit to pin.`
215
- : runnerSubstrate(args) === "local-desktop"
216
- ? `Subject app declared at ${publicAppUrl} (the host's loopback, opened from a browser on a local VM).`
217
- : `Subject app declared at ${publicAppUrl} (loopback inside the desktop sandbox).`,
207
+ // Declare what the subject was, including the absence of a pin. A desktop-cli product
208
+ // is named, since it has no URL. A local-app / in-process subject is an already-running
209
+ // local dev server the caller provisioned; it cannot be commit-pinned, so its provenance
210
+ // is unpinned and no E2B desktop was created. A plain app-url entry runs inside the
211
+ // desktop sandbox, or, on the local VM, reaches the host's loopback from the VM's browser.
212
+ message: subject.source === "desktop-cli"
213
+ ? `Subject product declared: ${subject.product}, used from a terminal window inside the desktop sandbox.`
214
+ : args.entryKind === "local-app"
215
+ ? `Subject app declared at ${publicAppUrl}: a running local dev server driven in this process, with no clone and no E2B desktop. Provenance: provided by the caller and unpinned, because a running dev server has no commit to pin.`
216
+ : runnerSubstrate(args) === "local-desktop"
217
+ ? `Subject app declared at ${publicAppUrl} (the host's loopback, opened from a browser on a local VM).`
218
+ : `Subject app declared at ${publicAppUrl} (loopback inside the desktop sandbox).`,
218
219
  }),
219
220
  args.session
220
221
  ? participantEvent(SINGLE, {
@@ -428,11 +429,7 @@ export function buildSingleParticipantBundle(args) {
428
429
  ...(args.providerResources === undefined || args.providerResources.length === 0
429
430
  ? {}
430
431
  : { providerResources: args.providerResources }),
431
- // Structured subject provenance: code pin + state story. Uniform, and
432
- // stated on app-url bundles too: the caller minted the URL, its state is the caller's.
433
- // CuaSubjectProvenanceArg's two variants (clone, local-tree) are already RunSubjectProvenance-
434
- // shaped, so no reconstruction is needed beyond the app-url fallback.
435
- subject: args.subjectProvenance ?? { source: "app-url", state: { provenance: "undeclared" } },
432
+ subject: args.subject,
436
433
  ...(cost === undefined ? {} : { cost }),
437
434
  };
438
435
  }
@@ -2,7 +2,7 @@ import type { ComputerUsePlan } from "../../study/plan-types.js";
2
2
  import type { StudySubjectState } from "../../study/types.js";
3
3
  import { type RunSubjectProvenance, type RunSubjectStateStepRecord } from "../../run/bundle.js";
4
4
  import { type LocalTreeArchive } from "../../subject/local-tree-archive.js";
5
- import { type DesktopParticipantRun, type CuaSubjectProjection, type CuaSubjectProvenanceArg, type ParticipantRunOutcome } from "./types.js";
5
+ import { type DesktopParticipantRun, type ParticipantRunOutcome } from "./types.js";
6
6
  /**
7
7
  * Each participant's subject projection. A participant's outcome adds the subject commit it resolved and the
8
8
  * state steps it executed; without outcomes (dry run, or a run still in progress) the declared
@@ -15,7 +15,7 @@ export declare function projectParticipantSubjects(args: {
15
15
  runs: readonly DesktopParticipantRun[];
16
16
  outcomes: readonly ParticipantRunOutcome[] | undefined;
17
17
  dryRun: boolean;
18
- }): CuaSubjectProjection[];
18
+ }): RunSubjectProvenance[];
19
19
  /**
20
20
  * Resolve the bundle's state marker from the declaration and what actually ran.
21
21
  * Precedence: external declared → "unpinned" (seed records, if any, stay attached; a
@@ -28,10 +28,6 @@ export declare function resolveSubjectState(args: {
28
28
  dryRun: boolean;
29
29
  executed: RunSubjectStateStepRecord[];
30
30
  }): RunSubjectProvenance["state"];
31
- /** Narrow a resolved CuaSubjectProjection into the shape buildSingleParticipantBundle's
32
- * subjectProvenance param wants (provisioned-route sources only; app-url stays undeclared, the
33
- * default branch that builder already handles). */
34
- export declare function subjectProvenanceArg(subject: CuaSubjectProjection, publicRepo: string | undefined, subjectEnvNames: string[]): CuaSubjectProvenanceArg | undefined;
35
31
  /**
36
32
  * The run-level subject for the top level and the bundle. Local-tree participants all pack from the
37
33
  * same once-per-run archive, so every participant already carries the identical archiveSha256/commit/dirty and
@@ -39,11 +35,11 @@ export declare function subjectProvenanceArg(subject: CuaSubjectProjection, publ
39
35
  * commit; the aggregate carries it only when every participant agrees, and warns when they diverge.
40
36
  */
41
37
  export declare function aggregateCuaSubject(args: {
42
- subjects: readonly CuaSubjectProjection[];
38
+ subjects: readonly RunSubjectProvenance[];
43
39
  outcomes: readonly ParticipantRunOutcome[] | undefined;
44
40
  participantCount: number;
45
41
  dryRun: boolean;
46
42
  }): {
47
- subject: CuaSubjectProjection;
43
+ subject: RunSubjectProvenance;
48
44
  warnings: string[];
49
45
  };
@@ -1,6 +1,6 @@
1
- // Each participant's subject projection and the subject-state marker, from the declared subject
2
- // and what the participants ran; the run-level subject aggregated from them; and the provenance argument the
3
- // bundle builders take.
1
+ // How a computer-use run records its subject: each participant's subject and the subject-state
2
+ // marker, from the declared subject and what the participants ran, and the run-level subject
3
+ // aggregated from them. Both bundle shapes and the JSON result write what this module returns.
4
4
  import { commandDigestOf } from "../../subject/state.js";
5
5
  import { cuaDeclaredState } from "./plan.js";
6
6
  import {} from "../../run/bundle.js";
@@ -25,7 +25,7 @@ export function projectParticipantSubjects(args) {
25
25
  executed: outcome?.stateStepRecords ?? [],
26
26
  });
27
27
  return participantSubjectProjection({
28
- kind: subject.kind,
28
+ subject,
29
29
  ...(publicRepo === undefined ? {} : { publicRepo }),
30
30
  subjectEnvNames,
31
31
  ...(outcome?.subjectCommit === undefined ? {} : { subjectCommit: outcome.subjectCommit }),
@@ -79,7 +79,7 @@ export function resolveSubjectState(args) {
79
79
  * commit/dirty (no divergence is possible, unlike the clone route's per-participant
80
80
  * in-sandbox commit). */
81
81
  function participantSubjectProjection(args) {
82
- if (args.kind === "clone" && args.publicRepo) {
82
+ if (args.subject.kind === "clone" && args.publicRepo) {
83
83
  return {
84
84
  source: "clone",
85
85
  repo: args.publicRepo,
@@ -88,7 +88,7 @@ function participantSubjectProjection(args) {
88
88
  state: args.subjectState,
89
89
  };
90
90
  }
91
- if (args.kind === "local-tree") {
91
+ if (args.subject.kind === "local-tree") {
92
92
  const archive = args.localTreeArchive;
93
93
  return {
94
94
  source: "local-tree",
@@ -100,32 +100,10 @@ function participantSubjectProjection(args) {
100
100
  state: args.subjectState,
101
101
  };
102
102
  }
103
- return { source: "app-url", state: args.subjectState };
104
- }
105
- /** Narrow a resolved CuaSubjectProjection into the shape buildSingleParticipantBundle's
106
- * subjectProvenance param wants (provisioned-route sources only; app-url stays undeclared, the
107
- * default branch that builder already handles). */
108
- export function subjectProvenanceArg(subject, publicRepo, subjectEnvNames) {
109
- if (subject.source === "clone" && publicRepo) {
110
- return {
111
- source: "clone",
112
- repo: publicRepo,
113
- ...(subject.commit === undefined ? {} : { commit: subject.commit }),
114
- envNames: subjectEnvNames,
115
- state: subject.state,
116
- };
103
+ if (args.subject.kind === "desktop-cli") {
104
+ return { source: "desktop-cli", product: args.subject.product.name, state: args.subjectState };
117
105
  }
118
- if (subject.source === "local-tree") {
119
- return {
120
- source: "local-tree",
121
- ...(subject.archiveSha256 === undefined ? {} : { archiveSha256: subject.archiveSha256 }),
122
- ...(subject.commit === undefined ? {} : { commit: subject.commit }),
123
- ...(subject.dirty === undefined ? {} : { dirty: subject.dirty }),
124
- envNames: subjectEnvNames,
125
- state: subject.state,
126
- };
127
- }
128
- return undefined;
106
+ return { source: "app-url", state: args.subjectState };
129
107
  }
130
108
  /**
131
109
  * The run-level subject for the top level and the bundle. Local-tree participants all pack from the
@@ -161,7 +161,7 @@ export interface CuaParticipantResult {
161
161
  killed: boolean;
162
162
  streamUrlPresent: boolean;
163
163
  };
164
- subject: CuaSubjectProjection;
164
+ subject: RunSubjectProvenance;
165
165
  diagnostics?: CuaDiagnostics;
166
166
  /** Set when the participant was skipped (pinned reason string). */
167
167
  skippedReason?: string;
@@ -185,44 +185,7 @@ export interface CuaParticipantSummary {
185
185
  concurrency: number;
186
186
  waves: number;
187
187
  }
188
- 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_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";
189
- /** Subject provenance projection: what the actor actually drove. */
190
- export interface CuaSubjectProjection {
191
- source: "app-url" | "clone" | "local-tree";
192
- /** Clone-route only: the (possibly redacted) owner/repo slug. */
193
- repo?: string;
194
- /** Cloned commit SHA (clone route) or host-side HEAD at pack time (local-tree route, when
195
- * the packed root was a git work tree). */
196
- commit?: string;
197
- /** Local-tree-route only: 64-hex sha256 over the sorted packed-entries list: the content
198
- * pin for a tree that cannot be commit-pinned. Absent on dry-run (nothing was packed). */
199
- archiveSha256?: string;
200
- /** Local-tree-route only: host-side porcelain status at pack time (true when the working
201
- * tree had uncommitted changes). Absent when the packed root was not a git work tree. */
202
- dirty?: boolean;
203
- /** Declared env names provisioned for the subject (values never surface anywhere). */
204
- envNames?: string[];
205
- /** The subject's state story (seeded digests / unpinned external / declared-not-run /
206
- * undeclared): the same block the run bundle records. */
207
- state: RunSubjectProvenance["state"];
208
- }
209
- /** The provisioned-route-only shape threaded through as buildSingleParticipantBundle's
210
- * subjectProvenance arg (clone or local-tree; an app-url subject stays undeclared, which that
211
- * builder's own default branch already handles without this type). */
212
- export type CuaSubjectProvenanceArg = {
213
- source: "clone";
214
- repo: string;
215
- commit?: string;
216
- envNames: string[];
217
- state: RunSubjectProvenance["state"];
218
- } | {
219
- source: "local-tree";
220
- archiveSha256?: string;
221
- commit?: string;
222
- dirty?: boolean;
223
- envNames: string[];
224
- state: RunSubjectProvenance["state"];
225
- };
188
+ 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";
226
189
  export interface CuaActorStudyResult extends AutomaticAnalysisResult, StudyResultIdentity<"computer-use"> {
227
190
  /** True when the Observer verified the bundle, all live participants passed credibility checks
228
191
  * (or this is a dry-run), and no declared adapter/scorer verdict failed. */
@@ -253,7 +216,7 @@ export interface CuaActorStudyResult extends AutomaticAnalysisResult, StudyResul
253
216
  };
254
217
  /** Subject provenance: what the actor actually drove. At N>1 this is the
255
218
  * unanimity-gated aggregate (top-level `commit` only when every participant resolved the same one). */
256
- subject?: CuaSubjectProjection;
219
+ subject?: RunSubjectProvenance;
257
220
  /** The pre-flight participant plan (present once participants resolve; absent on early validation errors). */
258
221
  plan?: CuaParticipantPlan;
259
222
  /** Per-participant results, always present once participants resolve (length 1 at N=1). */
@@ -485,8 +448,8 @@ export interface CuaFanoutBundleArgs {
485
448
  run: BundleRun;
486
449
  specs: DesktopParticipantRun[];
487
450
  outcomes?: ParticipantRunOutcome[];
488
- subjects: CuaSubjectProjection[];
489
- aggregateSubject: CuaSubjectProjection;
451
+ subjects: RunSubjectProvenance[];
452
+ aggregateSubject: RunSubjectProvenance;
490
453
  descriptor: CuaActorDescriptor;
491
454
  appUrl: string;
492
455
  dryRun: boolean;
@@ -5,7 +5,7 @@ import { parse as parseYaml } from "yaml";
5
5
  import path from "node:path";
6
6
  import { parseBrowserPersonaJourneyFromScenario } from "../../actors/scripted-browser/journey.js";
7
7
  import { digestText } from "../../evidence/redaction.js";
8
- import { readContainedRegularFile, } from "../../run/contained-output.js";
8
+ import { readContainedRegularFile, PROJECT_FILE_MAX_BYTES, refusalText, } from "../../run/contained-output.js";
9
9
  // Same public-safe token shape the study id uses; an id-style `scenario` must match it before
10
10
  // it is interpolated into a repo path.
11
11
  const SCENARIO_REF_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
@@ -20,6 +20,7 @@ export async function resolveScriptedScenario(projectRoot, ref) {
20
20
  const trimmed = ref.trim();
21
21
  let absolutePath;
22
22
  let source;
23
+ let read;
23
24
  if (scenarioRefLooksLikePath(trimmed)) {
24
25
  absolutePath = path.resolve(projectRoot.physicalPath, trimmed);
25
26
  const relative = path.relative(projectRoot.physicalPath, absolutePath);
@@ -42,22 +43,36 @@ export async function resolveScriptedScenario(projectRoot, ref) {
42
43
  path.posix.join("humanish", "scenarios", `${trimmed}.yaml`),
43
44
  path.posix.join("humanish", "scenarios", `${trimmed}.yml`),
44
45
  ];
45
- const found = await firstExistingFile(projectRoot, candidates);
46
- if (!found) {
46
+ const found = await firstScenarioFile(projectRoot, candidates);
47
+ if (found === null) {
47
48
  return {
48
49
  ok: false,
49
50
  message: `scenario "${trimmed}" was not found (looked for ${candidates.join(", ")}).`,
50
51
  };
51
52
  }
52
- source = found;
53
- absolutePath = path.join(projectRoot.physicalPath, found);
53
+ // A candidate that is there and refused stops the search: the next one is not the scenario.
54
+ if (found.read.status === "refused") {
55
+ return {
56
+ ok: false,
57
+ message: `scenario "${trimmed}" could not be read: ${refusalText(found.candidate, found.read)}.`,
58
+ };
59
+ }
60
+ source = found.candidate;
61
+ read = found.read;
62
+ absolutePath = path.join(projectRoot.physicalPath, found.candidate);
54
63
  }
55
64
  const relativeScenarioPath = path.relative(projectRoot.physicalPath, absolutePath);
56
- const scenarioBytes = await readContainedRegularFile(projectRoot, relativeScenarioPath);
57
- if (!scenarioBytes) {
65
+ read ??= await readContainedRegularFile(projectRoot, relativeScenarioPath, PROJECT_FILE_MAX_BYTES);
66
+ if (read.status === "refused" && read.reason === "too-large") {
67
+ return {
68
+ ok: false,
69
+ message: `scenario "${trimmed}" could not be read: ${refusalText(source, read)}.`,
70
+ };
71
+ }
72
+ if (read.status !== "read") {
58
73
  return { ok: false, message: `scenario "${trimmed}" could not be read (${source}).` };
59
74
  }
60
- const text = scenarioBytes.toString("utf8");
75
+ const text = read.bytes.toString("utf8");
61
76
  let raw;
62
77
  try {
63
78
  raw = parseYaml(text);
@@ -92,11 +107,12 @@ function scenarioRefLooksLikePath(ref) {
92
107
  ref.includes("\\") ||
93
108
  ref.startsWith("."));
94
109
  }
95
- async function firstExistingFile(projectRoot, candidates) {
110
+ /** The first candidate that is there, read or refused; null when none is. */
111
+ async function firstScenarioFile(projectRoot, candidates) {
96
112
  for (const candidate of candidates) {
97
- if ((await readContainedRegularFile(projectRoot, candidate)) !== null) {
98
- return candidate;
99
- }
113
+ const read = await readContainedRegularFile(projectRoot, candidate, PROJECT_FILE_MAX_BYTES);
114
+ if (read.status !== "missing")
115
+ return { candidate, read };
100
116
  }
101
117
  return null;
102
118
  }
@@ -2,7 +2,7 @@
2
2
  // path it names must be a safe path inside the run's artifact root. A result that fails is unsafe,
3
3
  // and the route stops without writing a bundle from it.
4
4
  import path from "node:path";
5
- import { readContainedRegularFile } from "../../run/contained-output.js";
5
+ import { readContainedRegularFile, refusalText, RUN_ARTIFACT_MAX_BYTES, } from "../../run/contained-output.js";
6
6
  export class UnsafeScriptedSessionResultError extends Error {
7
7
  constructor(message) {
8
8
  super(message);
@@ -16,8 +16,11 @@ export async function existingScreenshots(runPaths, result) {
16
16
  if (!step.screenshotPath) {
17
17
  continue;
18
18
  }
19
- const screenshot = await readContainedRegularFile(runPaths, step.screenshotPath);
20
- if (screenshot && screenshot.byteLength > 0) {
19
+ const screenshot = await readContainedRegularFile(runPaths, step.screenshotPath, RUN_ARTIFACT_MAX_BYTES);
20
+ // A screenshot that is there and refused would leave the bundle without a frame it took.
21
+ if (screenshot.status === "refused")
22
+ throw new UnsafeScriptedSessionResultError(`Scripted session screenshot ${refusalText(step.screenshotPath, screenshot)}.`);
23
+ if (screenshot.status === "read" && screenshot.bytes.byteLength > 0) {
21
24
  existing.push(step.screenshotPath);
22
25
  }
23
26
  }
@@ -6,7 +6,7 @@ import { participantEvent, participantRecord, participantStream, } from "../../r
6
6
  import { declaredScreenForRender } from "../../substrates/e2b/desktop-geometry.js";
7
7
  import { publicSafeRouteLabel } from "./provenance.js";
8
8
  import { participantCaption } from "../../run/participant-caption.js";
9
- import { sessionEndingInWords } from "../../run/outcomes.js";
9
+ import { participantSummary } from "../../run/outcomes.js";
10
10
  function participantView(ctx, spec, index) {
11
11
  const { args, external } = ctx;
12
12
  const name = participantCaption({ id: spec.planned.id, personaId: spec.persona.id });
@@ -66,13 +66,7 @@ function sharedWorldSimulation(ctx, spec, index, view) {
66
66
  mode: "browser-sim",
67
67
  progress: inProgress ? 35 : 100,
68
68
  currentStep: view.reason,
69
- summary: session
70
- ? `${name} used the shared app and ${sessionEndingInWords(session.trace)}.`
71
- : outcome?.sessionError !== undefined
72
- ? `${name} did not finish a session: ${outcome.sessionError}`
73
- : inProgress
74
- ? `${name} is using the shared app.`
75
- : `${name} would use the shared app; no session ran.`,
69
+ summary: participantSummary(name, { name: "the shared app" }, { trace: session?.trace, sessionError: outcome?.sessionError, inProgress }),
76
70
  startedAt: args.run.createdAt,
77
71
  updatedAt: args.run.createdAt,
78
72
  });
@@ -1,10 +1,8 @@
1
- import type { SharedWorldPlan } from "../../study/plan-types.js";
2
1
  import type { SharedWorldParticipant } from "../../study/plan-participants.js";
3
2
  import type { RunBundle } from "../../run/bundle.js";
4
3
  import type { DesktopParticipantRun, ParticipantRunOutcome } from "../computer-use/types.js";
5
4
  import { type DesktopParticipantDeps } from "../computer-use/participant-deps.js";
6
5
  import type { LiveParticipants, PlaneContext } from "./types.js";
7
- export declare function defaultSessionTimeoutMs(plan: SharedWorldPlan): number;
8
6
  /** Resolve an actor's participant URL against the harness-minted getHost base (entry is a
9
7
  * same-origin relative path, validated at parse against serve.url). */
10
8
  export declare function resolveActorEntryUrl(baseUrl: string, entry: string | undefined): string;
@@ -11,26 +11,7 @@ import { desktopParticipantDeps, } from "../computer-use/participant-deps.js";
11
11
  import { resolveCommittedPersonasForCwd } from "../../study/persona-resolve.js";
12
12
  import { participantAssignment } from "../../study/participant-assignment.js";
13
13
  import { redactText } from "../../evidence/redaction.js";
14
- import { MAX_SANDBOX_MS, SANDBOX_TIMEOUT_BUFFER_MS, SUBJECT_PROVISION_BUDGET_MS, } from "../../substrates/e2b/lifetime.js";
15
- import { DEFAULT_STATE_STEP_TIMEOUT_MS } from "../../subject/state.js";
16
- // The default per-participant session budget is derived from the route. On a provisioned route the
17
- // binding constraint is the subject sandbox (it must outlive every participant: timeoutMs +
18
- // provisioning + seeding + teardown buffer, and E2B refuses a sandbox over one hour), so the
19
- // derivation hands each participant the most that cap allows, capped at 15 minutes, floored at the
20
- // historical 300s so a seed-heavy study never gets less room than it always had. App-url
21
- // participants have no subject sandbox and default to 30 minutes (participant sandbox: 30m + 10m
22
- // buffer stays well under the hour). An explicit execution.timeoutMs is never adjusted. The handoff
23
- // latch scales off this (40%).
24
- const MAX_DERIVED_SESSION_MS = 15 * 60_000;
25
- const MIN_DERIVED_SESSION_MS = 300_000;
26
- const DEFAULT_APP_URL_SESSION_MS = 30 * 60_000;
27
- export function defaultSessionTimeoutMs(plan) {
28
- if (plan.plane.kind !== "provisioned")
29
- return DEFAULT_APP_URL_SESSION_MS;
30
- const stateBudgetMs = (plan.plane.subject.state.seed ?? []).reduce((sum, step) => sum + (step.timeoutMs ?? DEFAULT_STATE_STEP_TIMEOUT_MS), 0);
31
- const room = MAX_SANDBOX_MS - SUBJECT_PROVISION_BUDGET_MS - stateBudgetMs - SANDBOX_TIMEOUT_BUFFER_MS;
32
- return Math.max(MIN_DERIVED_SESSION_MS, Math.min(MAX_DERIVED_SESSION_MS, room));
33
- }
14
+ import { SANDBOX_TIMEOUT_BUFFER_MS } from "../../substrates/e2b/lifetime.js";
34
15
  // prose-check: model prompt (the participant model reads this, not a person)
35
16
  const DEFAULT_MISSION = "You are one of MANY users hitting a shared web application at the same time. The browser is already open at the app. Accomplish your role's task, then stop.";
36
17
  /** Resolve an actor's participant URL against the harness-minted getHost base (entry is a
@@ -1,6 +1,7 @@
1
1
  import { type CuaActorDescriptor } from "../../actors/registry.js";
2
2
  import type { RoutePlanResult, RouteRefusal, SharedWorldPlan } from "../../study/plan-types.js";
3
3
  import type { StudyConfig, StudySubjectState } from "../../study/types.js";
4
+ import { type SandboxCeiling } from "../../substrates/e2b/lifetime.js";
4
5
  import type { ConcurrentSharedWorldStudyErrorCode } from "./types.js";
5
6
  /** The error a shared-world study returns before a run starts. */
6
7
  export interface SharedWorldRefusal extends RouteRefusal<"shared-world", ConcurrentSharedWorldStudyErrorCode> {
@@ -18,6 +19,8 @@ export declare function planSharedWorldStudy(config: StudyConfig, input: {
18
19
  readonly dryRun: boolean;
19
20
  /** Whether deps.runSession is set: a caller's session runner cannot enforce maxOutputTokens. */
20
21
  readonly hasRunSession?: boolean;
22
+ /** The longest sandbox lifetime the operator's E2B plan allows (sandboxCeiling). */
23
+ readonly sandboxCeiling: SandboxCeiling;
21
24
  }): SharedWorldPlanResult;
22
25
  /** The provisioned plane's declared subject state. The external-public plane declares none. */
23
26
  export declare function planeStateOf(plan: SharedWorldPlan): StudySubjectState | undefined;
@@ -10,8 +10,10 @@ import { brainOf, planCaps, desktopRequirements, isNonEmpty, planBase, } from ".
10
10
  import { sharedWorldParticipants } from "../../study/plan-participants.js";
11
11
  import { PUBLIC_TARGET_OWNER_PATTERN } from "../../study/parse/subject.js";
12
12
  import { REPO_SLUG_PATTERN } from "../../study/parse/values.js";
13
- import { concurrentSharedWorldValidationReason, desktopMediaValidationReason, externalPublicSharedWorldValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, taskProtocolValidationReason, } from "../../study/validation.js";
13
+ import { concurrentSharedWorldValidationReason, desktopMediaValidationReason, externalPublicSharedWorldValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, taskProtocolValidationReason, waitLimitValidationReason, } from "../../study/validation.js";
14
14
  import { unpricedCapCheck } from "../../study/requirements.js";
15
+ import { DEFAULT_STATE_STEP_TIMEOUT_MS } from "../../subject/state.js";
16
+ import { SANDBOX_TIMEOUT_BUFFER_MS, SUBJECT_PROVISION_BUDGET_MS, } from "../../substrates/e2b/lifetime.js";
15
17
  /** The registered descriptor for a planned actor; planSharedWorldStudy checked the registry. */
16
18
  export function sharedWorldDescriptorOf(actorType) {
17
19
  const descriptor = actorRegistry[actorType];
@@ -53,6 +55,7 @@ export function planSharedWorldStudy(config, input) {
53
55
  // A library caller's config skips the parser, which refuses these first.
54
56
  studyUrlCredentialReason(config) ??
55
57
  outputTokenLimitValidationReason(config) ??
58
+ waitLimitValidationReason(config) ??
56
59
  (externalPublic
57
60
  ? externalPublicSharedWorldValidationReason(config)
58
61
  : concurrentSharedWorldValidationReason(config));
@@ -87,6 +90,9 @@ export function planSharedWorldStudy(config, input) {
87
90
  // subject.env on an app-url subject for the same reason.
88
91
  if (externalPublic && config.subject.env !== undefined)
89
92
  return refuse(invalid, "`subject.env` applies only to clone subjects or local-tree subjects (the served app's environment channel).", actor);
93
+ const ceiling = input.sandboxCeiling;
94
+ if (!ceiling.ok)
95
+ return refuse(invalid, ceiling.message, actor);
90
96
  const plane = planeOf(config);
91
97
  if (plane === undefined)
92
98
  throw new Error("shared-world validation admitted a plane it cannot plan");
@@ -105,9 +111,7 @@ export function planSharedWorldStudy(config, input) {
105
111
  actor,
106
112
  plane,
107
113
  concurrency: config.execution?.concurrency ?? plane.participants.length,
108
- ...(config.execution?.timeoutMs === undefined
109
- ? {}
110
- : { sessionTimeoutMs: config.execution.timeoutMs }),
114
+ sessionTimeoutMs: config.execution?.timeoutMs ?? defaultSessionTimeoutMs(plane, ceiling.ms),
111
115
  brain,
112
116
  caps: planCaps(config),
113
117
  requirements: base.dryRun
@@ -128,6 +132,23 @@ export function planSharedWorldStudy(config, input) {
128
132
  },
129
133
  };
130
134
  }
135
+ // The default per-participant session budget is derived from the plane. On a provisioned plane the
136
+ // binding constraint is the subject sandbox (it must outlive every participant: timeoutMs +
137
+ // provisioning + seeding + teardown buffer, under the sandbox ceiling), so the derivation hands each
138
+ // participant the most the ceiling allows, capped at 15 minutes, floored at the historical 300s so
139
+ // a seed-heavy study never gets less room than it always had. App-url participants have no subject
140
+ // sandbox and default to 30 minutes (participant sandbox: 30m + 10m buffer stays under the hour).
141
+ // An explicit execution.timeoutMs is never adjusted. The handoff latch scales off this (40%).
142
+ const MAX_DERIVED_SESSION_MS = 15 * 60_000;
143
+ const MIN_DERIVED_SESSION_MS = 300_000;
144
+ const DEFAULT_APP_URL_SESSION_MS = 30 * 60_000;
145
+ function defaultSessionTimeoutMs(plane, ceilingMs) {
146
+ if (plane.kind !== "provisioned")
147
+ return DEFAULT_APP_URL_SESSION_MS;
148
+ const stateBudgetMs = (plane.subject.state.seed ?? []).reduce((sum, step) => sum + (step.timeoutMs ?? DEFAULT_STATE_STEP_TIMEOUT_MS), 0);
149
+ const room = ceilingMs - SUBJECT_PROVISION_BUDGET_MS - stateBudgetMs - SANDBOX_TIMEOUT_BUFFER_MS;
150
+ return Math.max(MIN_DERIVED_SESSION_MS, Math.min(MAX_DERIVED_SESSION_MS, room));
151
+ }
131
152
  /** The provisioned plane's declared subject state. The external-public plane declares none. */
132
153
  export function planeStateOf(plan) {
133
154
  return plan.plane.kind === "provisioned" ? plan.plane.subject.state : undefined;
@@ -9,7 +9,7 @@ import { prepareEmailReceiving, prepareExternalComms, receivingSourceOf, subject
9
9
  import { declaredOriginDigestOf } from "./external-public.js";
10
10
  import { packSubjectTree } from "./provisioned.js";
11
11
  import { emptyPlaneResults } from "./result.js";
12
- import { buildParticipantSpecs, defaultSessionTimeoutMs } from "./participant-specs.js";
12
+ import { buildParticipantSpecs } from "./participant-specs.js";
13
13
  import { planeStateOf } from "./plan.js";
14
14
  import path from "node:path";
15
15
  import { e2bRequestTimeoutMs } from "../../substrates/e2b/lifetime.js";
@@ -87,7 +87,7 @@ export async function prepareConcurrentRun(study, scope) {
87
87
  const { run } = started;
88
88
  const { runId, createdAt, paths: runPaths, source } = run;
89
89
  const artifactRoot = runPaths.absoluteRunRoot;
90
- const timeoutMs = plan.sessionTimeoutMs ?? defaultSessionTimeoutMs(plan);
90
+ const timeoutMs = plan.sessionTimeoutMs;
91
91
  const requestTimeoutMs = e2bRequestTimeoutMs(env);
92
92
  const redactScreenshots = plan.residual.policies?.redactScreenshots === true;
93
93
  const timers = deps.detachedTimers ?? {};
@@ -1,21 +1,21 @@
1
- import type { ActorTokenUsage } from "../../actors/contract.js";
1
+ import type { ActorTokenUsage, ActorTrace } from "../../actors/contract.js";
2
2
  import type { StudyCaps } from "../../study/types.js";
3
3
  import { type CostCategory } from "../../run/terminal-contract.js";
4
4
  import type { CostLine, NoSpendProof, TerminalCostLedger } from "./types.js";
5
5
  /**
6
6
  * Build the spend ledger from the captured session. The null discipline:
7
7
  * - The `provider` line is populated from the actor trace's tokenUsage.costUsd when the trace
8
- * carries it (a measured value, incl. a measured 0). When the trace carries no costUsd, the
9
- * provider line is `null` = not measured (never guessed to 0 just because no-spend was intended).
8
+ * carries it (a measured value, incl. a measured 0). Otherwise its `usd` is null = not
9
+ * measured, and when the trace's `estimatedCost` prices the tokens, the line carries that
10
+ * price as `estimatedUsd`: the figure run.json's model-tokens line gives, from the same
11
+ * object. Tokens with no rate, and a run with no token count, say so in the note.
10
12
  * - product/media/payment are `null` by default: core has no signal for those categories, and
11
13
  * only a test can supply one, through `StudyDeps.costProbe`.
12
14
  * `injectedLines` lets a test supply known spend for a category,
13
15
  * exercising the fail-closed cap enforcement deterministically without a real billable run.
14
16
  */
15
17
  export declare function buildCostLedger(args: {
16
- tokenCostUsd?: number;
17
- /** Measured token counts, when the run produced them but no rate could price them. */
18
- tokenUsage?: ActorTokenUsage;
18
+ trace: Pick<ActorTrace, "tokenUsage" | "estimatedCost">;
19
19
  injectedLines?: Partial<Record<CostCategory, CostLine>>;
20
20
  }): TerminalCostLedger;
21
21
  /** True when no cost line carries a dollar value, so a no-spend proof has nothing to stand on. */
@@ -26,9 +26,9 @@ export declare function noSpendLineMeasured(proof: Pick<NoSpendProof, "knownZero
26
26
  */
27
27
  export declare function noSpendNotEstablished(maxUsd: number): string;
28
28
  /**
29
- * What the ledger measured, in words: the dollar lines it knows, provider tokens it counted but
30
- * could not price, and the lines it has no signal for. A reader of a maxUsd=0 run must not take
31
- * an all-null ledger for a proven $0.
29
+ * What the ledger measured, in words: the dollar lines it knows, the provider tokens it counted
30
+ * with their estimate or as unpriced, and the lines it has no signal for. A reader of a maxUsd=0
31
+ * run must not take an all-null ledger for a proven $0.
32
32
  */
33
33
  export declare function describeMeasuredSpend(ledger: TerminalCostLedger, tokenUsage?: ActorTokenUsage): string;
34
34
  /** Derive the no-spend proof from the ledger. It vouches for known-zero lines and