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
package/CHANGELOG.md CHANGED
@@ -8,6 +8,47 @@ The Unreleased section holds the full notes for the next version until it is tag
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 0.117.0: Long participant waits, OpenAI wait length, serve.build fails fast, TUI confirmations (2026-10-09)
12
+
13
+ humanish 0.117.0 lets a participant wait through a long hosted study, gives an OpenAI
14
+ participant's wait a usable length, and fails a broken `serve.build` within seconds. A
15
+ participant's wait longer than one desktop call now lasts as long as it asked, up to the new study
16
+ field `actor.maxWaitMs` (1 second to 10 minutes; 2 minutes by default, 30 seconds on a desktop with
17
+ speech), and goes to the desktop in steps of at most 30 seconds. A Codex participant's longer wait
18
+ was cut to 30 seconds before. An `openai-computer-use` participant's `wait` names no duration and
19
+ lasted half a second on a hosted desktop; it now lasts `actor.idleWaitMs` (5 seconds by default) in
20
+ a turn that only waits, and 500 ms after an action in the same turn. The participant's `actor.json`
21
+ records these lengths as `waitSettings`. A `serve.install`, `serve.build` or seed step that bash
22
+ cannot parse fails within seconds with bash's error, where the run waited out the step's limit (10
23
+ minutes for `serve.build`), and a failed `serve.build` on the computer-use route has its own error
24
+ code. In `humanish tui`, a two-Enter confirmation ends when the cursor leaves it. The terminal
25
+ route's check after its sandbox kill carries the run's E2B key, and browser Back in the Observer
26
+ clears a stale saved-moment message.
27
+
28
+ [Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.117.0)
29
+
30
+ ## 0.116.0: Hosted keyboard input, E2B sandbox ceiling, bounded run reads, run header, TUI live starts (2026-10-09)
31
+
32
+ humanish 0.116.0 delivers a hosted participant's arrow keys and punctuation, lets the operator
33
+ raise the E2B sandbox lifetime, and reads run files within size limits. On hosted E2B desktops,
34
+ `ARROWDOWN`, Page Up, Page Down, `?`, `/` and other names missing from `@e2b/desktop`'s key table
35
+ reached xdotool unchanged, which ignored them while the run recorded the keypress as completed;
36
+ both desktops now translate key names through one table and refuse a name outside it.
37
+ `HUMANISH_E2B_MAX_SANDBOX_MINUTES` sets the longest sandbox lifetime the planners allow, 60 minutes
38
+ when unset and up to 1440 on E2B's Pro plan. A refusal names the value that admits the study, and
39
+ `humanish doctor` shows the ceiling. A route's sandbox kill, and the kills, checks and listings of
40
+ `humanish reclaim`, carry the run's E2B key. `verify` reads at most 32 MiB of a run file and lists
41
+ a larger file, or a run past 10,000 entries, as unscanned, and every contained read tells a missing
42
+ file from one it refused. A run whose analysis did not complete reads `live run finished; the
43
+ analysis did not complete`, a run refused before it was created prints no `analysis:` line, and a
44
+ `desktop-cli` run names its product on the `subject:` line and in run.json. In `humanish tui`, the
45
+ cursor follows the selected row through a refresh, a live start of a `mode: dry-run` file says
46
+ `needs mode: live`, Run again on a live run asks for a second Enter, the confirm prompt restates
47
+ the study's cost, and `c` lists every provider key. `NO_UPDATE_NOTIFIER` turns the update check
48
+ off.
49
+
50
+ [Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.116.0)
51
+
11
52
  ## 0.115.0: Closing report limits, admitted analysis cost, concurrent reviewer notes, known-value scrub (2026-10-08)
12
53
 
13
54
  humanish 0.115.0 asks the OpenAI computer-use participant for its closing report and impressions
package/README.md CHANGED
@@ -276,9 +276,10 @@ sensitive to capture in the first place.
276
276
  **What the automated gate enforces.** `humanish verify` scans public-bound
277
277
  artifacts and fails closed on secret, key, and token shapes and on known local
278
278
  path shapes. It reads text only, judged by a file's bytes rather than its name:
279
- any file in the run folder that is not UTF-8 text, other than a stream
280
- screenshot or recording, keeps the run `local_only` (`UNSCANNED_ARTIFACT`). It
281
- matches after undoing escapes, percent-encoding, HTML entities and base64. It
279
+ any file in the run folder that is not UTF-8 text or is larger than 32 MiB,
280
+ other than a stream screenshot or recording, keeps the run `local_only`
281
+ (`UNSCANNED_ARTIFACT`). It matches after undoing escapes, percent-encoding, HTML
282
+ entities and base64. It
282
283
  does not yet detect free-form PII or PHI such as names, emails,
283
284
  phone numbers, dates of birth, or medical identifiers. Keeping those out depends
284
285
  on using synthetic data and on review, so `redaction: passed` means the
@@ -483,9 +484,9 @@ At most once a day, a command run at a terminal also asks the npm registry for
483
484
  the latest humanish version, and a later command prints one line on stderr when
484
485
  yours is older. That request is a plain `GET` of humanish's dist-tags with no
485
486
  machine id, version, command or path. It is skipped in CI, without a terminal,
486
- for `--json`, and with `DO_NOT_TRACK=1`, `HUMANISH_TELEMETRY_DISABLED=1` or
487
- `HUMANISH_NO_UPDATE_CHECK=1`. [TELEMETRY.md](TELEMETRY.md#the-update-check)
488
- describes it.
487
+ for `--json`, and with `DO_NOT_TRACK=1`, `HUMANISH_TELEMETRY_DISABLED=1`,
488
+ `HUMANISH_NO_UPDATE_CHECK=1` or update-notifier's `NO_UPDATE_NOTIFIER=1`.
489
+ [TELEMETRY.md](TELEMETRY.md#the-update-check) describes it.
489
490
 
490
491
  ## Contribute
491
492
 
package/TELEMETRY.md CHANGED
@@ -132,4 +132,10 @@ participant, and when any of these is set:
132
132
  DO_NOT_TRACK=1
133
133
  HUMANISH_TELEMETRY_DISABLED=1
134
134
  HUMANISH_NO_UPDATE_CHECK=1
135
+ NO_UPDATE_NOTIFIER=1
135
136
  ```
137
+
138
+ `NO_UPDATE_NOTIFIER` is the variable the update-notifier package reads, so a
139
+ machine set up to silence the CLIs built on it gets no humanish notice either.
140
+ humanish reads it as update-notifier does: any value turns the check off,
141
+ including an empty one, `0` or `false`.
@@ -2,7 +2,7 @@ import { createServer } from "node:http";
2
2
  import path from "node:path";
3
3
  import { digestText } from "../../evidence/redaction.js";
4
4
  import { CODEX_APP_SERVER_ARTIFACTS, CODEX_APP_SERVER_DIRECTORY, runCodexAppServerSessionInPreparedRoot, } from "./app-server.js";
5
- import { prepareContainedOutputDirectory, prepareContainedOutputFile, prepareManagedHumanishOutputDirectory, prepareSelectedOutputDirectory, prepareSelectedOutputFile, readContainedRegularFile, writeContainedOutputFile, writePreparedSelectedOutputFile, } from "../../run/contained-output.js";
5
+ import { prepareContainedOutputDirectory, prepareContainedOutputFile, prepareManagedHumanishOutputDirectory, prepareSelectedOutputDirectory, prepareSelectedOutputFile, readContainedRegularFile, refusalText, RUN_ARTIFACT_MAX_BYTES, writeContainedOutputFile, writePreparedSelectedOutputFile, } from "../../run/contained-output.js";
6
6
  const CODEX_APP_SERVER_UI_SCHEMA = "humanish.codex-app-server-ui.v1";
7
7
  export async function startCodexAppServerUi(options) {
8
8
  const cwd = path.resolve(options.cwd);
@@ -128,13 +128,20 @@ export async function startCodexAppServerUi(options) {
128
128
  };
129
129
  }
130
130
  async function serveArtifact(args) {
131
- const body = await readContainedRegularFile(args.runRoot, args.requestPath);
132
- if (body) {
131
+ const read = await readContainedRegularFile(args.runRoot, args.requestPath, RUN_ARTIFACT_MAX_BYTES);
132
+ if (read.status === "read") {
133
133
  args.response.writeHead(200, {
134
134
  "cache-control": "no-store",
135
135
  "content-type": contentTypeFor(args.requestPath),
136
136
  });
137
- args.response.end(body);
137
+ args.response.end(read.bytes);
138
+ return;
139
+ }
140
+ // A file over the limit is there: a 404 would tell the page it does not exist. A link, or a
141
+ // path outside the run, is served as absent, as it always was.
142
+ if (read.status === "refused" && read.reason === "too-large") {
143
+ args.response.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
144
+ args.response.end(`${refusalText(args.requestPath, read)}.`);
138
145
  return;
139
146
  }
140
147
  args.response.writeHead(404);
@@ -10,8 +10,6 @@ export declare const PARTICIPANT_LIMITS: Readonly<{
10
10
  narration: 2000;
11
11
  output: number;
12
12
  actions: 4;
13
- /** The longest wait one action lasts; a longer one is shortened to it. */
14
- waitMs: 30000;
15
13
  requestMs: 180000;
16
14
  cleanupMs: 5000;
17
15
  }>;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { BROWSER_CONTROL_LIMITS, browserControlActionSchema, browserOnlyControlActionSchema, validateBrowserControlAction, } from "../../browser-control/protocol.js";
2
+ import { participantActionSchema, participantSpeechActionSchema, } from "../../browser-control/protocol.js";
3
3
  import { closingReportSchema, strictOutputSchema, } from "../closing-report.js";
4
4
  /** The declared profile. A participant replaces cliVersion with its detected CLI release, or with
5
5
  * this host's newest qualified release before its first launch. */
@@ -21,15 +21,13 @@ export const PARTICIPANT_LIMITS = Object.freeze({
21
21
  narration: 2000,
22
22
  output: 256 * 1024,
23
23
  actions: 4,
24
- /** The longest wait one action lasts; a longer one is shortened to it. */
25
- waitMs: BROWSER_CONTROL_LIMITS.waitMs,
26
24
  requestMs: 180_000,
27
25
  cleanupMs: 5000,
28
26
  });
29
27
  const toolInput = (speechEnabled) => z.strictObject({
30
28
  narration: z.string().max(PARTICIPANT_LIMITS.narration),
31
29
  actions: z
32
- .array(speechEnabled ? browserControlActionSchema : browserOnlyControlActionSchema)
30
+ .array(speechEnabled ? participantSpeechActionSchema : participantActionSchema)
33
31
  .min(1)
34
32
  .max(PARTICIPANT_LIMITS.actions),
35
33
  });
@@ -43,41 +41,15 @@ const participantFinal = z.strictObject({
43
41
  ...closingReportSchema.shape,
44
42
  });
45
43
  export const PARTICIPANT_FINAL_SCHEMA = strictOutputSchema(participantFinal);
46
- /**
47
- * Shorten each wait longer than one browser-control request may wait. A failed tool call ends the
48
- * native run, so a wait that asks too much would otherwise end the whole session.
49
- */
50
- function shortenWaits(value) {
51
- const actions = value?.actions;
52
- if (typeof value !== "object" || !Array.isArray(actions))
53
- return { value, shortened: [] };
54
- const shortened = [];
55
- const limit = PARTICIPANT_LIMITS.waitMs;
56
- const next = actions.map((action, index) => {
57
- const wait = action;
58
- if (wait?.kind !== "wait" ||
59
- typeof wait.ms !== "number" ||
60
- !Number.isFinite(wait.ms) ||
61
- wait.ms <= limit)
62
- return action;
63
- shortened.push({ index, requestedMs: wait.ms, ms: limit });
64
- return { ...wait, ms: limit };
65
- });
66
- return shortened.length === 0
67
- ? { value, shortened }
68
- : { value: { ...value, actions: next }, shortened };
69
- }
70
44
  /** humanish tool arguments, validated before the shared executor sees a batch. */
71
45
  export function parseParticipantTool(value, speechEnabled = false) {
72
46
  if (Buffer.byteLength(JSON.stringify(value) ?? "") > PARTICIPANT_LIMITS.output)
73
47
  throw new Error("invalid_response");
74
- const { value: admitted, shortened } = shortenWaits(value);
75
- const v = toolInput(speechEnabled).parse(admitted);
48
+ const v = toolInput(speechEnabled).parse(value);
76
49
  if (Buffer.byteLength(v.narration) > PARTICIPANT_LIMITS.narration)
77
50
  throw new Error("invalid_response");
78
51
  return {
79
- actions: v.actions.map(validateBrowserControlAction),
80
- ...(shortened.length === 0 ? {} : { shortenedWaits: shortened }),
52
+ actions: v.actions,
81
53
  pendingSafetyChecks: [],
82
54
  done: false,
83
55
  ...(v.narration ? { message: v.narration } : {}),
@@ -17,6 +17,11 @@ export interface RestrictedParticipantOptions {
17
17
  reasoningEffort?: ReasoningEffort;
18
18
  /** Admit speech actions and heard-speaker evidence for a speech-capable desktop only. */
19
19
  speechEnabled?: boolean;
20
+ /**
21
+ * The study's actor.maxWaitMs, which the loop applies and the tool description states. Default
22
+ * defaultMaxWaitMs(speechEnabled), the same default the loop applies.
23
+ */
24
+ maxWaitMs?: number;
20
25
  }
21
26
  /** What doctor's hosted-participant check found, before any turn. */
22
27
  export interface ParticipantReadiness {
@@ -3,6 +3,7 @@ import { defaultCodexCliVersion, untestedOperatorReleaseWarning } from "./codex-
3
3
  import { IMPRESSIONS_ASK, } from "../computer-use/loop.js";
4
4
  import { ComputerUseProviderError, isComputerUseProviderError, } from "../computer-use/provider-error.js";
5
5
  import { validateBrowserControlPng, validateHeardSpeech } from "../../browser-control/protocol.js";
6
+ import { defaultMaxWaitMs } from "../computer-use/wait.js";
6
7
  import { createRestrictedCodexSession, } from "./restricted-session.js";
7
8
  import { PARTICIPANT_PROFILE, PARTICIPANT_LIMITS as L, PARTICIPANT_FINAL_SCHEMA, participantToolSchema, parseParticipantTool, parseParticipantFinal, } from "./restricted-participant-policy.js";
8
9
  import { truncatedFrameWarning, unknownNotificationsWarning } from "./restricted-notifications.js";
@@ -40,7 +41,7 @@ function deferred() {
40
41
  return { promise, resolve, reject };
41
42
  }
42
43
  // prose-check: model prompt (Codex reads this tool description, not a person)
43
- const toolDescription = (speechEnabled) => `Act on the participant's browser through humanish. Submit one to four UI actions${speechEnabled ? ", including speak when you need to reply aloud," : ""} and a short public comment; never private reasoning. humanish returns a JSON STRING with execution acknowledgments${speechEnabled ? ", speech heard from the actual participant speaker sink," : ""} and a fresh screenshot. In Code Mode use: const r = JSON.parse(await tools.humanish_ui({narration: "...", actions: [...]})); text({acknowledgments: r.acknowledgments${speechEnabled ? ", heardSpeech: r.heardSpeech" : ""}, contextHint: r.contextHint, closing: r.closing}); image(r.imageUrl). Call serially and inspect each returned screenshot${speechEnabled ? " and heardSpeech array" : ""} before deciding what to do next. A wait action lasts at most ${L.waitMs} ms; a longer wait is shortened to ${L.waitMs} ms, so wait longer with another call.${speechEnabled ? " Use speak only after the visible UI shows that you joined the call and your microphone is unmuted; it sends audio into that call." : ""} Acknowledged input does not prove an application outcome. If closing is true, stop calling tools and give your final account.`;
44
+ const toolDescription = (speechEnabled, maxWaitMs) => `Act on the participant's browser through humanish. Submit one to four UI actions${speechEnabled ? ", including speak when you need to reply aloud," : ""} and a short public comment; never private reasoning. humanish returns a JSON STRING with execution acknowledgments${speechEnabled ? ", speech heard from the actual participant speaker sink," : ""} and a fresh screenshot. In Code Mode use: const r = JSON.parse(await tools.humanish_ui({narration: "...", actions: [...]})); text({acknowledgments: r.acknowledgments${speechEnabled ? ", heardSpeech: r.heardSpeech" : ""}, contextHint: r.contextHint, closing: r.closing}); image(r.imageUrl). Call serially and inspect each returned screenshot${speechEnabled ? " and heardSpeech array" : ""} before deciding what to do next. A wait action lasts at most ${maxWaitMs} ms; a longer wait is shortened to ${maxWaitMs} ms, so wait longer with another call.${speechEnabled ? " Use speak only after the visible UI shows that you joined the call and your microphone is unmuted; it sends audio into that call." : ""} Acknowledged input does not prove an application outcome. If closing is true, stop calling tools and give your final account.`;
44
45
  const participantInstructions = (prompt, speechEnabled) => `${prompt}\n\nYou are the study participant throughout this conversation, including its closing account. Use only the supplied screenshots and humanish_ui tool to interact. The tool returns a JSON string: parse it, inspect acknowledgments${speechEnabled ? " and heardSpeech captured from the actual participant speaker sink" : ""}, and display imageUrl with Code Mode image(). Do not print the image data URL as text. Keep your persona and earlier observations throughout the session.${speechEnabled ? " A speak action plays into the participant microphone; use it only after the visible UI shows that you joined the call and the microphone is unmuted." : ""} Speak publicly about your experience, never reveal private reasoning. Completed inputs do not prove application outcomes; verify on the next screenshot. When the task ends, return only the required final JSON with outcome, summary, frictionReports and impressions. Report observed confusion and recovered mistakes as well as blockers. ${IMPRESSIONS_ASK} Do not invent observations.`;
45
46
  /** Turns and failures from the native run, handed in order to the next provider request. */
46
47
  class ParticipantEvents {
@@ -191,7 +192,7 @@ function participantSession(options, speechEnabled, effort, call) {
191
192
  reasoningEffort: effort,
192
193
  tool: {
193
194
  name: "humanish_ui",
194
- description: toolDescription(speechEnabled),
195
+ description: toolDescription(speechEnabled, options.maxWaitMs ?? defaultMaxWaitMs(speechEnabled)),
195
196
  inputSchema: participantToolSchema(speechEnabled),
196
197
  call,
197
198
  },
@@ -38,6 +38,10 @@ export interface CuaActorSessionOptions {
38
38
  acknowledgeSafetyChecks?: (checks: CuaSafetyCheck[]) => CuaSafetyCheck[] | null;
39
39
  idleSteps?: number;
40
40
  noProgressSteps?: number;
41
+ /** The longest one wait action lasts, forwarded to the loop. See LoopRunOptions.maxWaitMs. */
42
+ maxWaitMs?: number;
43
+ /** How long a wait with no duration lasts in an idle turn. See LoopRunOptions.idleWaitMs. */
44
+ idleWaitMs?: number;
41
45
  /**
42
46
  * Redact persisted screenshots (blur+downscale). Default `false`: full fidelity for local use.
43
47
  * Set true for unowned subjects or share-as-is bundles. The provider always sees raw frames.
@@ -31,6 +31,8 @@ export async function runCuaActorSession(options) {
31
31
  ...(options.signal === undefined ? {} : { signal: options.signal }),
32
32
  ...(options.idleSteps === undefined ? {} : { idleSteps: options.idleSteps }),
33
33
  ...(options.noProgressSteps === undefined ? {} : { noProgressSteps: options.noProgressSteps }),
34
+ ...(options.maxWaitMs === undefined ? {} : { maxWaitMs: options.maxWaitMs }),
35
+ ...(options.idleWaitMs === undefined ? {} : { idleWaitMs: options.idleWaitMs }),
34
36
  ...(options.acknowledgeSafetyChecks === undefined
35
37
  ? {}
36
38
  : { acknowledgeSafetyChecks: options.acknowledgeSafetyChecks }),
@@ -1,5 +1,5 @@
1
1
  import type { LoopSession } from "./session.js";
2
- import type { CuaAction, CuaTurnRequest, ShortenedWait } from "./types.js";
2
+ import type { CuaAction, CuaTurnRequest } from "./types.js";
3
3
  /** A public-safe one-line action label. Never includes raw typed text. */
4
4
  export declare function describeCuaAction(action: CuaAction): string;
5
5
  /**
@@ -16,8 +16,8 @@ export interface ActionBatch {
16
16
  /** Host input acknowledgements for the provider's next request. */
17
17
  readonly execution: NonNullable<CuaTurnRequest["previousExecution"]>;
18
18
  /**
19
- * Guidance for the next request, set when the provider shortened a wait or a pre-dispatch
20
- * rejection stopped the batch.
19
+ * Guidance for the next request, set when the loop shortened a wait or a pre-dispatch rejection
20
+ * stopped the batch.
21
21
  */
22
22
  readonly hint: string | undefined;
23
23
  }
@@ -26,4 +26,4 @@ export interface ActionBatch {
26
26
  * leaves the desktop usable; a failed desktop command skips its action; any other failure ends
27
27
  * the session.
28
28
  */
29
- export declare function runActionBatch(session: LoopSession, actions: readonly CuaAction[], shortenedWaits?: readonly ShortenedWait[]): Promise<ActionBatch>;
29
+ export declare function runActionBatch(session: LoopSession, actions: readonly CuaAction[]): Promise<ActionBatch>;
@@ -3,6 +3,7 @@ import { commandFailureInfo, isCommandExitError } from "../../../substrates/comm
3
3
  import { ComputerUseExecutorError, isComputerUseExecutorError, } from "../executor-error.js";
4
4
  import { CuaAbortError, CuaDeadlineError, CuaStallError, raceCallBound, raceSessionDeadline, requestScope, } from "./race.js";
5
5
  import { notice } from "./trace.js";
6
+ import { planWait } from "../wait.js";
6
7
  // The participant's actions: their public labels, what counts as a material action, and how one
7
8
  // turn's batch is dispatched to the desktop.
8
9
  /** Click/scroll coordinates are bucketed to this many pixels before fingerprinting, so a one-pixel
@@ -109,31 +110,31 @@ function rejectedActionHint(title, reason) {
109
110
  const remedy = reason === undefined ? "" : ` ${REJECTION_REMEDIES[reason]}`;
110
111
  return `Your action (${title}) was rejected before dispatch. No input from that action or the rest of its batch was sent.${remedy} Choose your next action from the fresh screenshot; do not assume the rejected action succeeded.`;
111
112
  }
112
- function shortenedWaitHint(wait) {
113
- return `Your wait of ${wait.requestedMs}ms was shortened to ${wait.ms}ms, the longest one wait action lasts. To keep waiting, send another wait.`;
114
- }
115
- /** Record a wait the provider shortened, and return the participant's note about it. */
116
- function recordShortenedWait(session, title, wait) {
117
- session.trace.record("notice", () => notice("warn", "wait shortened", `action: ${title}; requested: ${wait.requestedMs}ms; waited: ${wait.ms}ms, the longest one wait action lasts; the session continued`));
118
- return shortenedWaitHint(wait);
113
+ /** Record a wait the loop shortened, and return the participant's note about it. */
114
+ function recordShortenedWait(session, title, requestedMs) {
115
+ const ms = session.waits.maxWaitMs;
116
+ session.trace.record("notice", () => notice("warn", "wait shortened", `action: ${title}; requested: ${requestedMs}ms; waited: ${ms}ms, the longest one wait action lasts (actor.maxWaitMs); the session continued`));
117
+ return `Your wait of ${requestedMs}ms was shortened to ${ms}ms, the longest one wait action lasts. To keep waiting, send another wait.`;
119
118
  }
120
119
  /**
121
120
  * Dispatch one turn's actions in order. A declared pre-dispatch rejection stops the batch and
122
121
  * leaves the desktop usable; a failed desktop command skips its action; any other failure ends
123
122
  * the session.
124
123
  */
125
- export async function runActionBatch(session, actions, shortenedWaits = []) {
124
+ export async function runActionBatch(session, actions) {
126
125
  const execution = { actions: [] };
127
126
  const { actionHistory, trace } = session;
128
127
  const notes = [];
129
128
  const hint = () => (notes.length === 0 ? undefined : notes.join(" "));
130
- for (const [index, action] of actions.entries()) {
129
+ const idleTurn = isIdleTurn(actions);
130
+ for (const [index, requested] of actions.entries()) {
131
131
  if (session.signal?.aborted)
132
132
  throw new CuaAbortError();
133
+ const wait = requested.kind === "wait" ? planWait(session.waits, requested.ms, idleTurn) : undefined;
134
+ const action = wait === undefined ? requested : { kind: "wait", ms: wait.ms };
133
135
  const title = describeCuaAction(action);
134
- const shortened = shortenedWaits.find((wait) => wait.index === index);
135
- if (shortened !== undefined)
136
- notes.push(recordShortenedWait(session, title, shortened));
136
+ if (wait?.shortenedFromMs !== undefined)
137
+ notes.push(recordShortenedWait(session, title, wait.shortenedFromMs));
137
138
  actionHistory.lastActionTitle = title;
138
139
  actionHistory.recentActionTitles.push(title);
139
140
  if (actionHistory.recentActionTitles.length > RECENT_ACTION_TITLES)
@@ -146,7 +147,7 @@ export async function runActionBatch(session, actions, shortenedWaits = []) {
146
147
  session.phase = `executing ${title}`;
147
148
  let status;
148
149
  try {
149
- status = await dispatchAction(session, action, title);
150
+ status = await dispatchAction(session, action, title, wait?.steps);
150
151
  }
151
152
  catch (error) {
152
153
  const declared = isComputerUseExecutorError(error)
@@ -201,17 +202,23 @@ function countAttempt(session, action, title, status) {
201
202
  }
202
203
  /**
203
204
  * Execute one action. Idle actions only look: a `wait` that hangs inside the SDK has in
204
- * effect waited, so a stalled one is skipped with a notice and loses nothing the participant
205
- * chose. An action is recorded as completed only after execute() resolves.
205
+ * effect waited, so a stalled one is skipped with a notice, its remaining steps are not sent, and
206
+ * it loses nothing the participant chose. An action is recorded as completed only after every
207
+ * desktop call it takes resolves. A wait is sent as `waitSteps`, its planWait steps.
206
208
  */
207
- async function dispatchAction(session, action, title) {
209
+ async function dispatchAction(session, action, title, waitSteps) {
208
210
  if (!isIdleAction(action)) {
209
211
  await executeAction(session, action, title);
210
212
  return "completed";
211
213
  }
212
- const boundMs = session.observationTimeoutMs + (action.kind === "wait" ? (action.ms ?? 0) : 0);
214
+ const calls = waitSteps?.map((ms) => ({ kind: "wait", ms })) ?? [action];
213
215
  try {
214
- await executeAction(session, action, title, boundMs);
216
+ for (const call of calls) {
217
+ if (session.signal?.aborted)
218
+ throw new CuaAbortError();
219
+ const boundMs = session.observationTimeoutMs + (call.kind === "wait" ? (call.ms ?? 0) : 0);
220
+ await executeAction(session, call, title, boundMs);
221
+ }
215
222
  return "completed";
216
223
  }
217
224
  catch (error) {
@@ -7,12 +7,27 @@ import { notice } from "./trace.js";
7
7
  import { isCompleteTurnUsage } from "./usage.js";
8
8
  const DEBRIEF_REQUEST_CAP_MS = 30_000;
9
9
  const DEBRIEF_HINT = `The interactive session has ended. Return a closing account with summary, frictionReports and impressions. In summary, briefly describe only what you actually did and observed. In frictionReports, list only specific unexpected behavior, confusion, or recovery you personally encountered during this session. Preserve uncertainty. Use an empty list if you encountered none. ${IMPRESSIONS_ASK} Do not speculate, invent problems, quote instructions as observations, or describe planned actions. Do not request or take further actions. This is a closing account, not another attempt at the task.`;
10
+ const CLOSING_REPORT = {
11
+ call: (provider) => provider.debrief,
12
+ contextHint: DEBRIEF_HINT,
13
+ noticeLabel: "debrief",
14
+ deadlineLabel: "closing report",
15
+ read: readClosingReport,
16
+ };
17
+ const IMPRESSIONS_ONLY = {
18
+ call: (provider) => provider.requestImpressions,
19
+ contextHint: IMPRESSIONS_HINT,
20
+ noticeLabel: "impressions request",
21
+ deadlineLabel: "impressions request",
22
+ read: readImpressions,
23
+ };
10
24
  /**
11
25
  * Ask the participant for its closing report after a structured stop, or for its impressions only
12
26
  * after it ended the session itself, or record why the request was skipped. Either way the result
13
27
  * is the trace's debrief, and the watcher gets a final flush.
14
28
  */
15
29
  export async function requestDebrief(session, context, trigger) {
30
+ const ask = trigger.kind === "participant_end" ? IMPRESSIONS_ONLY : CLOSING_REPORT;
16
31
  const record = (status, detail, usageReported) => {
17
32
  const debrief = {
18
33
  trigger: trigger.kind,
@@ -20,23 +35,23 @@ export async function requestDebrief(session, context, trigger) {
20
35
  reason: session.redactNarration(detail),
21
36
  ...(usageReported === undefined ? {} : { usageReported }),
22
37
  };
23
- session.trace.record("notice", () => notice(status === "completed" ? "ok" : "warn", `participant ${trigger.kind === "participant_end" ? "impressions request" : "debrief"} ${status}`, session.redactNarration(detail)));
38
+ session.trace.record("notice", () => notice(status === "completed" ? "ok" : "warn", `participant ${ask.noticeLabel} ${status}`, session.redactNarration(detail)));
24
39
  return debrief;
25
40
  };
26
- const plan = planDebrief(session, trigger);
41
+ const plan = planDebrief(session, ask, trigger);
27
42
  const debrief = "skip" in plan
28
43
  ? record("skipped", plan.skip)
29
- : await debriefExchange(session, plan.ask, context, trigger, plan.boundMs, record);
44
+ : await debriefExchange(session, context, trigger, ask, plan, record);
30
45
  session.flush();
31
46
  return debrief;
32
47
  }
33
- function planDebrief(session, trigger) {
48
+ function planDebrief(session, ask, trigger) {
34
49
  const { provider, signal, usage } = session;
35
50
  const { maxUsd, overRunBudget, estimateTurnCostUsd } = session.settings;
36
51
  if (session.trace.counts.turns === 0)
37
52
  return { skip: "the study stopped before any participant turn" };
38
- const ask = trigger.kind === "participant_end" ? provider.requestImpressions : provider.debrief;
39
- if (ask === undefined)
53
+ const send = ask.call(provider)?.bind(provider);
54
+ if (send === undefined)
40
55
  return { skip: "this provider does not support read-only closing reports" };
41
56
  if (usage.cleanupUnconfirmed)
42
57
  return { skip: "participant request cleanup is unconfirmed" };
@@ -68,9 +83,9 @@ function planDebrief(session, trigger) {
68
83
  skip: signal?.aborted ? "the study was cancelled" : "the session deadline was reached",
69
84
  };
70
85
  }
71
- return { ask, boundMs };
86
+ return { send, boundMs };
72
87
  }
73
- async function debriefExchange(session, ask, context, trigger, boundMs, record) {
88
+ async function debriefExchange(session, context, trigger, ask, { send, boundMs }, record) {
74
89
  const { signal, provider } = session;
75
90
  const scope = requestScope(signal);
76
91
  const timer = setTimeout(() => scope.end(), boundMs);
@@ -89,12 +104,12 @@ async function debriefExchange(session, ask, context, trigger, boundMs, record)
89
104
  ...(context.acknowledgedSafetyChecks === undefined
90
105
  ? {}
91
106
  : { acknowledgedSafetyChecks: context.acknowledgedSafetyChecks }),
92
- contextHint: trigger.kind === "participant_end" ? IMPRESSIONS_HINT : DEBRIEF_HINT,
107
+ contextHint: ask.contextHint,
93
108
  };
94
109
  const turn = provider.requestPolicy === "fail_closed"
95
- ? await singleDispatch(session, "debrief", (dispatchSignal) => ask.call(provider, request, dispatchSignal), boundMs)
96
- : await raceSessionDeadline(ask.call(provider, request, scope.signal), boundMs, signal);
97
- return acceptDebriefTurn(session, turn, trigger, record);
110
+ ? await singleDispatch(session, "debrief", (dispatchSignal) => send(request, dispatchSignal), boundMs)
111
+ : await raceSessionDeadline(send(request, scope.signal), boundMs, signal);
112
+ return acceptDebriefTurn(session, turn, ask, record);
98
113
  }
99
114
  catch (error) {
100
115
  // A failed optional report cannot rewrite the already observed structured completion.
@@ -104,7 +119,7 @@ async function debriefExchange(session, ask, context, trigger, boundMs, record)
104
119
  const detail = signal?.aborted
105
120
  ? "cancelled"
106
121
  : scope.signal.aborted || error instanceof CuaDeadlineError
107
- ? `${trigger.kind === "participant_end" ? "impressions request" : "closing report"} deadline reached`
122
+ ? `${ask.deadlineLabel} deadline reached`
108
123
  : error instanceof Error
109
124
  ? error.message
110
125
  : String(error);
@@ -117,7 +132,7 @@ async function debriefExchange(session, ask, context, trigger, boundMs, record)
117
132
  scope.end();
118
133
  }
119
134
  }
120
- function acceptDebriefTurn(session, turn, trigger, record) {
135
+ function acceptDebriefTurn(session, turn, ask, record) {
121
136
  const { maxUsd, overRunBudget, estimateTurnCostUsd } = session.settings;
122
137
  session.usage.record(turn, "debrief");
123
138
  session.lastResponseId = turn.responseId ?? session.lastResponseId;
@@ -133,19 +148,17 @@ function acceptDebriefTurn(session, turn, trigger, record) {
133
148
  return record("failed", "the closing response requested actions or safety checks; none were executed and its report was not accepted", usageReported);
134
149
  }
135
150
  // First-party providers parse their replies already; these parses cover third-party ports.
136
- if (trigger.kind === "participant_end") {
137
- const impressions = participantImpressionsSchema.safeParse(turn.impressions);
138
- if (!impressions.success) {
139
- return record("failed", turn.interruption === "output_limit"
140
- ? "the reply was cut off by the output limit"
141
- : "the reply did not contain valid impressions", usageReported);
142
- }
143
- session.impressions = recordImpressions(session, impressions.data);
144
- return record("completed", "one read-only impressions request; no additional desktop actions; the participant's own ending is unchanged", usageReported);
145
- }
151
+ const { status, detail, ...added } = ask.read(session, turn);
152
+ return { ...record(status, detail, usageReported), ...added };
153
+ }
154
+ /** A closing report: its summary and friction as one participant message, then its impressions. */
155
+ function readClosingReport(session, turn) {
146
156
  const closing = closingReportSchema.safeParse(turn.closingReport);
147
157
  if (!closing.success) {
148
- return record("failed", "the closing response did not contain a valid structured participant report", usageReported);
158
+ return {
159
+ status: "failed",
160
+ detail: "the closing response did not contain a valid structured participant report",
161
+ };
149
162
  }
150
163
  const report = {
151
164
  summary: session.redactNarration(closing.data.summary.trim()),
@@ -160,6 +173,27 @@ function acceptDebriefTurn(session, turn, trigger, record) {
160
173
  }));
161
174
  session.trace.bump("messages");
162
175
  session.impressions = recordImpressions(session, closing.data.impressions);
163
- const debrief = record("completed", "one read-only report; no additional desktop actions; original stop and task outcomes preserved", usageReported);
164
- return { ...debrief, report, messageId };
176
+ return {
177
+ status: "completed",
178
+ detail: "one read-only report; no additional desktop actions; original stop and task outcomes preserved",
179
+ report,
180
+ messageId,
181
+ };
182
+ }
183
+ /** Impressions alone, after the participant ended the session itself. */
184
+ function readImpressions(session, turn) {
185
+ const impressions = participantImpressionsSchema.safeParse(turn.impressions);
186
+ if (!impressions.success) {
187
+ return {
188
+ status: "failed",
189
+ detail: turn.interruption === "output_limit"
190
+ ? "the reply was cut off by the output limit"
191
+ : "the reply did not contain valid impressions",
192
+ };
193
+ }
194
+ session.impressions = recordImpressions(session, impressions.data);
195
+ return {
196
+ status: "completed",
197
+ detail: "one read-only impressions request; no additional desktop actions; the participant's own ending is unchanged",
198
+ };
165
199
  }
@@ -5,6 +5,7 @@ import type { DebriefTrigger, Stop } from "./ending.js";
5
5
  import { TraceRecorder } from "./trace.js";
6
6
  import type { CuaExecutor, CuaProvider, CuaSafetyCheck, LoopRunOptions } from "./types.js";
7
7
  import { UsageLedger } from "./usage.js";
8
+ import { type WaitSettings } from "../wait.js";
8
9
  export type ScreenshotRef = NonNullable<ActorTraceItem["screenshotRef"]>;
9
10
  /**
10
11
  * The options with no default, read once at entry and passed through. Defaulted options and
@@ -34,6 +35,8 @@ export declare class LoopSession {
34
35
  readonly timeoutMs: number;
35
36
  readonly idleSteps: number;
36
37
  readonly noProgressSteps: number;
38
+ /** How long each wait lasts: the study's wait fields or their defaults. */
39
+ readonly waits: WaitSettings;
37
40
  readonly redactScreenshots: boolean;
38
41
  readonly now: () => number;
39
42
  readonly acknowledgeSafetyChecks: (checks: CuaSafetyCheck[]) => CuaSafetyCheck[] | null;
@@ -1,6 +1,7 @@
1
1
  import { TaskTracker } from "../../../study/tasks.js";
2
2
  import { TraceRecorder } from "./trace.js";
3
3
  import { UsageLedger } from "./usage.js";
4
+ import { resolveWaitSettings } from "../wait.js";
4
5
  // The state one loop session shares across its phases: the options as read at entry, the session
5
6
  // clock, the trace being recorded, the usage ledger, and what the participant has done so far.
6
7
  // Waiting is a legitimate strategy. A persona verifying a sign-up by email polls its inbox
@@ -24,6 +25,8 @@ export class LoopSession {
24
25
  timeoutMs;
25
26
  idleSteps;
26
27
  noProgressSteps;
28
+ /** How long each wait lasts: the study's wait fields or their defaults. */
29
+ waits;
27
30
  redactScreenshots;
28
31
  // Injected functions are wrapped so they are called without a receiver, as plain functions.
29
32
  now;
@@ -67,7 +70,7 @@ export class LoopSession {
67
70
  scrubText;
68
71
  constructor(options) {
69
72
  // Read once, in this order: a caller mutating its options object mid-run changes nothing.
70
- const { instructions, provider, executor, persona, redaction, timeoutMs, now, signal, idleSteps = DEFAULT_IDLE_STEPS, noProgressSteps = DEFAULT_NO_PROGRESS_STEPS, acknowledgeSafetyChecks = () => null, redactScreenshots = false, scrubText = (text) => text, writeScreenshot = async (name) => `screenshots/${name}`, stopWhen, dwell, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), tasks, maxUsd, overRunBudget, estimateTurnCostUsd, requireReportedUsageForSpendCap = false, onObservedUrl, onMessage, onScreenshot, onTrace, } = options;
73
+ const { instructions, provider, executor, persona, redaction, timeoutMs, now, signal, idleSteps = DEFAULT_IDLE_STEPS, noProgressSteps = DEFAULT_NO_PROGRESS_STEPS, maxWaitMs, idleWaitMs, acknowledgeSafetyChecks = () => null, redactScreenshots = false, scrubText = (text) => text, writeScreenshot = async (name) => `screenshots/${name}`, stopWhen, dwell, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), tasks, maxUsd, overRunBudget, estimateTurnCostUsd, requireReportedUsageForSpendCap = false, onObservedUrl, onMessage, onScreenshot, onTrace, } = options;
71
74
  this.settings = {
72
75
  instructions,
73
76
  persona,
@@ -89,6 +92,11 @@ export class LoopSession {
89
92
  this.timeoutMs = timeoutMs;
90
93
  this.idleSteps = idleSteps;
91
94
  this.noProgressSteps = noProgressSteps;
95
+ this.waits = resolveWaitSettings({
96
+ maxWaitMs,
97
+ idleWaitMs,
98
+ speechEnabled: executor.speechEnabled === true,
99
+ });
92
100
  this.redactScreenshots = redactScreenshots;
93
101
  this.now = () => now();
94
102
  this.acknowledgeSafetyChecks = (checks) => acknowledgeSafetyChecks(checks);
@@ -123,6 +123,7 @@ export function loopResult(session, stop, debrief) {
123
123
  : { maxOutputTokens: provider.modelSettings.maxOutputTokens }),
124
124
  },
125
125
  }),
126
+ waitSettings: { ...session.waits },
126
127
  counts,
127
128
  items: recorder.items,
128
129
  ...(session.actionHistory.affordances.length > 0