humanish 0.116.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.
- package/CHANGELOG.md +19 -0
- package/dist/actors/codex/restricted-participant-policy.d.ts +0 -2
- package/dist/actors/codex/restricted-participant-policy.js +4 -32
- package/dist/actors/codex/restricted-participant.d.ts +5 -0
- package/dist/actors/codex/restricted-participant.js +3 -2
- package/dist/actors/computer-use/actor.d.ts +4 -0
- package/dist/actors/computer-use/actor.js +2 -0
- package/dist/actors/computer-use/loop/actions.d.ts +4 -4
- package/dist/actors/computer-use/loop/actions.js +25 -18
- package/dist/actors/computer-use/loop/session.d.ts +3 -0
- package/dist/actors/computer-use/loop/session.js +9 -1
- package/dist/actors/computer-use/loop/trace.js +1 -0
- package/dist/actors/computer-use/loop/types.d.ts +16 -14
- package/dist/actors/computer-use/loop.d.ts +1 -1
- package/dist/actors/computer-use/loop.js +1 -1
- package/dist/actors/computer-use/wait.d.ts +74 -0
- package/dist/actors/computer-use/wait.js +100 -0
- package/dist/actors/contract.d.ts +14 -0
- package/dist/browser-control/protocol.d.ts +86 -6
- package/dist/browser-control/protocol.js +13 -6
- package/dist/guest/desktop-executor.js +3 -1
- package/dist/observer-app.html +7 -7
- package/dist/routes/computer-use/e2b-desktop/prepare.js +40 -31
- package/dist/routes/computer-use/local-vm.js +5 -2
- package/dist/routes/computer-use/participant-model.js +9 -0
- package/dist/routes/computer-use/plan.js +4 -2
- package/dist/routes/computer-use/types.d.ts +1 -1
- package/dist/routes/shared-world/plan.js +2 -1
- package/dist/routes/terminal/live-sandbox.d.ts +1 -1
- package/dist/routes/terminal/live-sandbox.js +14 -11
- package/dist/routes/terminal/sandbox.d.ts +4 -3
- package/dist/routes/terminal/sandbox.js +5 -5
- package/dist/study/config.js +4 -1
- package/dist/study/keys.d.ts +2 -0
- package/dist/study/keys.js +2 -0
- package/dist/study/migrate/v2.js +2 -1
- package/dist/study/parse/actors.js +8 -0
- package/dist/study/plan-participants.d.ts +4 -0
- package/dist/study/plan-participants.js +4 -0
- package/dist/study/types.d.ts +14 -0
- package/dist/study/validation.d.ts +5 -0
- package/dist/study/validation.js +8 -0
- package/dist/study/warnings.js +12 -0
- package/dist/subject/serve.d.ts +3 -0
- package/dist/subject/serve.js +4 -1
- package/dist/substrates/detached.js +14 -7
- package/dist/substrates/e2b/desktop-executor.d.ts +0 -2
- package/dist/substrates/e2b/desktop-executor.js +3 -3
- package/dist/tui-app.js +117 -116
- package/docs/architecture/browser-control.md +10 -4
- package/docs/contracts/schemas.md +19 -11
- package/package.json +2 -2
|
@@ -5,6 +5,7 @@ import { validatePreparedRunArtifactPaths } from "../../../run/paths.js";
|
|
|
5
5
|
import { provisionCloneSubject } from "../../../subject/clone.js";
|
|
6
6
|
import { provisionDesktopCli } from "../../../subject/desktop-cli.js";
|
|
7
7
|
import { provisionLocalTreeSubject } from "../../../subject/local-tree.js";
|
|
8
|
+
import { SubjectBuildError } from "../../../subject/serve.js";
|
|
8
9
|
import { inspectDesktopScreenGeometry } from "../../../substrates/e2b/desktop-geometry.js";
|
|
9
10
|
import { observeDesktopResources } from "../../../substrates/e2b/desktop-resources.js";
|
|
10
11
|
import { acquireE2BDesktopSandbox, e2bDesktopTemplate } from "../../../substrates/e2b/sandbox.js";
|
|
@@ -166,37 +167,45 @@ export async function provisionParticipantSubject(ctx, state, desktop) {
|
|
|
166
167
|
onPhase: ctx.onSubjectPhase,
|
|
167
168
|
});
|
|
168
169
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
170
|
+
try {
|
|
171
|
+
if (subject.kind === "clone") {
|
|
172
|
+
state.subjectCommit = await provisionCloneSubject(shell, {
|
|
173
|
+
repo: subject.repo,
|
|
174
|
+
depth: residual.subject.clone?.depth ?? 1,
|
|
175
|
+
serve: subject.serve,
|
|
176
|
+
...(subject.state === undefined ? {} : { state: subject.state }),
|
|
177
|
+
hasGithubToken: subject.env.includes("GITHUB_TOKEN"),
|
|
178
|
+
requestTimeoutMs: deps.requestTimeoutMs,
|
|
179
|
+
scrub: deps.scrubKnownValues,
|
|
180
|
+
onCommit: (commit) => {
|
|
181
|
+
state.subjectCommit = commit;
|
|
182
|
+
},
|
|
183
|
+
onStateStep: (record) => {
|
|
184
|
+
state.stateStepRecords.push(record);
|
|
185
|
+
},
|
|
186
|
+
onPhase: ctx.onSubjectPhase,
|
|
187
|
+
...deps.detachedTimers,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else if (subject.kind === "local-tree" && deps.localTreeArchiveBuffer) {
|
|
191
|
+
await provisionLocalTreeSubject(shell, {
|
|
192
|
+
archiveBuffer: deps.localTreeArchiveBuffer,
|
|
193
|
+
serve: subject.serve,
|
|
194
|
+
...(subject.state === undefined ? {} : { state: subject.state }),
|
|
195
|
+
requestTimeoutMs: deps.requestTimeoutMs,
|
|
196
|
+
scrub: deps.scrubKnownValues,
|
|
197
|
+
onStateStep: (record) => {
|
|
198
|
+
state.stateStepRecords.push(record);
|
|
199
|
+
},
|
|
200
|
+
onPhase: ctx.onSubjectPhase,
|
|
201
|
+
...deps.detachedTimers,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
187
204
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
requestTimeoutMs: deps.requestTimeoutMs,
|
|
194
|
-
scrub: deps.scrubKnownValues,
|
|
195
|
-
onStateStep: (record) => {
|
|
196
|
-
state.stateStepRecords.push(record);
|
|
197
|
-
},
|
|
198
|
-
onPhase: ctx.onSubjectPhase,
|
|
199
|
-
...deps.detachedTimers,
|
|
200
|
-
});
|
|
205
|
+
catch (error) {
|
|
206
|
+
// A failed serve.build gets its own code, so the run names the step the author has to fix.
|
|
207
|
+
if (error instanceof SubjectBuildError)
|
|
208
|
+
state.failureCode = "HUMANISH_COMPUTER_USE_SUBJECT_BUILD_FAILED";
|
|
209
|
+
throw error;
|
|
201
210
|
}
|
|
202
211
|
}
|
|
@@ -139,10 +139,11 @@ function createLocalParticipantDesktop(context, run, warnings, artifactRoot) {
|
|
|
139
139
|
};
|
|
140
140
|
}
|
|
141
141
|
/** The Codex account participant, whose unconfirmed cleanup blocks the study's analysis. */
|
|
142
|
-
function accountProvider(state) {
|
|
142
|
+
function accountProvider(state, maxWaitMs) {
|
|
143
143
|
return async ({ executor }) => {
|
|
144
144
|
const participant = createRestrictedCodexParticipant({
|
|
145
145
|
speechEnabled: executor.speechEnabled === true,
|
|
146
|
+
...(maxWaitMs === undefined ? {} : { maxWaitMs }),
|
|
146
147
|
});
|
|
147
148
|
state.participants.push(participant);
|
|
148
149
|
const provider = Object.assign(participant.provider, {
|
|
@@ -183,7 +184,9 @@ export function prepareLocalVmRun(options) {
|
|
|
183
184
|
};
|
|
184
185
|
const { config: _config, assets: _assets, signal: _signal, ...runOptions } = options;
|
|
185
186
|
return {
|
|
186
|
-
options: account
|
|
187
|
+
options: account
|
|
188
|
+
? { ...runOptions, createProvider: accountProvider(state, config.actor?.maxWaitMs) }
|
|
189
|
+
: runOptions,
|
|
187
190
|
localVm: {
|
|
188
191
|
desktop: (run, warnings, artifactRoot) => createLocalParticipantDesktop(context, run, warnings, artifactRoot),
|
|
189
192
|
capacity: () => localRuntimeCapacity(),
|
|
@@ -44,6 +44,9 @@ export async function startParticipantModel(spec, deps, executor) {
|
|
|
44
44
|
: { reasoningEffort: spec.planned.limits.reasoningEffort }),
|
|
45
45
|
...(brain.declaredModel === undefined ? {} : { model: brain.declaredModel }),
|
|
46
46
|
...(executor.speechEnabled === true ? { speechEnabled: true } : {}),
|
|
47
|
+
...(spec.planned.limits.maxWaitMs === undefined
|
|
48
|
+
? {}
|
|
49
|
+
: { maxWaitMs: spec.planned.limits.maxWaitMs }),
|
|
47
50
|
session: { env },
|
|
48
51
|
});
|
|
49
52
|
return { provider: codexParticipant.provider, codexParticipant };
|
|
@@ -130,6 +133,12 @@ export function participantSessionOptions(spec, deps, ready, provider, writeScre
|
|
|
130
133
|
? {}
|
|
131
134
|
: { stopWhen: spec.planned.limits.stopWhen }),
|
|
132
135
|
...(spec.planned.limits.dwell === undefined ? {} : { dwell: spec.planned.limits.dwell }),
|
|
136
|
+
...(spec.planned.limits.maxWaitMs === undefined
|
|
137
|
+
? {}
|
|
138
|
+
: { maxWaitMs: spec.planned.limits.maxWaitMs }),
|
|
139
|
+
...(spec.planned.limits.idleWaitMs === undefined
|
|
140
|
+
? {}
|
|
141
|
+
: { idleWaitMs: spec.planned.limits.idleWaitMs }),
|
|
133
142
|
...(spec.planned.tasks === undefined ? {} : { tasks: spec.planned.tasks }),
|
|
134
143
|
// The study budget: this participant notes its own running estimate on the shared
|
|
135
144
|
// ledger and stops when the run total crosses the cap, independent of the per-participant
|
|
@@ -10,7 +10,7 @@ import { subjectStateInvalidReason } from "../../study/parse/subject-state.js";
|
|
|
10
10
|
import { brainOf, callerBrainOf, planCaps, desktopRequirements, planBase, provisionedSubject, } from "../../study/plan-base.js";
|
|
11
11
|
import { computerUseParticipants, declaredTargets } from "../../study/plan-participants.js";
|
|
12
12
|
import { MAX_COMPUTER_USE_PARTICIPANTS } from "../../study/routing.js";
|
|
13
|
-
import { cloneTargetValidationReason, computerUseValidationReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, taskProtocolValidationReason, } from "../../study/validation.js";
|
|
13
|
+
import { cloneTargetValidationReason, computerUseValidationReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, taskProtocolValidationReason, waitLimitValidationReason, } from "../../study/validation.js";
|
|
14
14
|
import { desktopCliProductReason } from "../../study/composition-rules.js";
|
|
15
15
|
import { isLocalBrowserStudy } from "../../substrates/local/runtime-config.js";
|
|
16
16
|
import { boundedConcurrency, defaultSessionTimeoutMs, resolveParticipantSandboxMs, } from "./participant-runs.js";
|
|
@@ -83,7 +83,9 @@ function cuaStudyRejection(config, hasRunSession, driving, subjectRoute, ceiling
|
|
|
83
83
|
}
|
|
84
84
|
/** A declaration this route cannot honor, for any subject or with the caller's own driver. */
|
|
85
85
|
function unsupportedDeclarationReason(config, hasRunSession, driving, { inProcessRoute }) {
|
|
86
|
-
const reason = desktopMediaValidationReason(config) ||
|
|
86
|
+
const reason = desktopMediaValidationReason(config) ||
|
|
87
|
+
outputTokenLimitValidationReason(config) ||
|
|
88
|
+
waitLimitValidationReason(config);
|
|
87
89
|
if (reason)
|
|
88
90
|
return invalid(reason);
|
|
89
91
|
if (config.actor?.maxOutputTokens !== undefined &&
|
|
@@ -185,7 +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";
|
|
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";
|
|
189
189
|
export interface CuaActorStudyResult extends AutomaticAnalysisResult, StudyResultIdentity<"computer-use"> {
|
|
190
190
|
/** True when the Observer verified the bundle, all live participants passed credibility checks
|
|
191
191
|
* (or this is a dry-run), and no declared adapter/scorer verdict failed. */
|
|
@@ -10,7 +10,7 @@ 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
15
|
import { DEFAULT_STATE_STEP_TIMEOUT_MS } from "../../subject/state.js";
|
|
16
16
|
import { SANDBOX_TIMEOUT_BUFFER_MS, SUBJECT_PROVISION_BUDGET_MS, } from "../../substrates/e2b/lifetime.js";
|
|
@@ -55,6 +55,7 @@ export function planSharedWorldStudy(config, input) {
|
|
|
55
55
|
// A library caller's config skips the parser, which refuses these first.
|
|
56
56
|
studyUrlCredentialReason(config) ??
|
|
57
57
|
outputTokenLimitValidationReason(config) ??
|
|
58
|
+
waitLimitValidationReason(config) ??
|
|
58
59
|
(externalPublic
|
|
59
60
|
? externalPublicSharedWorldValidationReason(config)
|
|
60
61
|
: concurrentSharedWorldValidationReason(config));
|
|
@@ -11,6 +11,7 @@ import { extractLocalActorVerdict, normalizeLocalActorTranscript, } from "../../
|
|
|
11
11
|
import { NODE_BOOTSTRAP_COMMAND, NODE_BOOTSTRAP_TIMEOUT_MS } from "../../subject/node-bootstrap.js";
|
|
12
12
|
import { observeDesktopResources, } from "../../substrates/e2b/desktop-resources.js";
|
|
13
13
|
import { acquireE2BShellSandbox } from "../../substrates/e2b/sandbox.js";
|
|
14
|
+
import { e2bAccount } from "../../substrates/e2b/connection.js";
|
|
14
15
|
import { E2BDesktopStartupError, loadE2BDesktopModule, } from "../../substrates/e2b/sdk.js";
|
|
15
16
|
import { shellQuote } from "../../substrates/shell.js";
|
|
16
17
|
import { buildOpenAiEgressNetwork } from "./runtime-auth.js";
|
|
@@ -36,7 +37,7 @@ export class LiveTerminalSandbox {
|
|
|
36
37
|
wallClockMs;
|
|
37
38
|
sandboxTimeoutMs;
|
|
38
39
|
sandbox;
|
|
39
|
-
|
|
40
|
+
account;
|
|
40
41
|
allocation;
|
|
41
42
|
startupCleanup;
|
|
42
43
|
startupCleanupDetail;
|
|
@@ -60,7 +61,6 @@ export class LiveTerminalSandbox {
|
|
|
60
61
|
// Declared egress allowlist, or undefined for the historical unrestricted default.
|
|
61
62
|
const egressAllow = plan.egressAllow;
|
|
62
63
|
const sandboxModule = await (deps.desktopModule ?? loadE2BDesktopModule)();
|
|
63
|
-
this.module = sandboxModule;
|
|
64
64
|
await validatePreparedRunArtifactPaths(runPaths);
|
|
65
65
|
// No sandbox-global env in either mode. In openai-egress, only this host-side SDK request
|
|
66
66
|
// carries the real runtime key; participant commands receive an inert placeholder. The proxy
|
|
@@ -71,16 +71,19 @@ export class LiveTerminalSandbox {
|
|
|
71
71
|
const network = runtimeEnv.mode === "openai-egress"
|
|
72
72
|
? buildOpenAiEgressNetwork(runtimeEnv.keyValue, routing)
|
|
73
73
|
: routing;
|
|
74
|
+
const options = {
|
|
75
|
+
apiKey: e2bApiKey,
|
|
76
|
+
requestTimeoutMs,
|
|
77
|
+
timeoutMs: sandboxTimeoutMs,
|
|
78
|
+
metadata,
|
|
79
|
+
...(network === undefined ? {} : { network }),
|
|
80
|
+
lifecycle: { onTimeout: "kill" },
|
|
81
|
+
};
|
|
82
|
+
// Teardown's check by id reaches the account the create used, whatever process.env names.
|
|
83
|
+
this.account = e2bAccount(sandboxModule, options);
|
|
74
84
|
const acquired = await acquireE2BShellSandbox({
|
|
75
85
|
module: sandboxModule,
|
|
76
|
-
options
|
|
77
|
-
apiKey: e2bApiKey,
|
|
78
|
-
requestTimeoutMs,
|
|
79
|
-
timeoutMs: sandboxTimeoutMs,
|
|
80
|
-
metadata,
|
|
81
|
-
...(network === undefined ? {} : { network }),
|
|
82
|
-
lifecycle: { onTimeout: "kill" },
|
|
83
|
-
},
|
|
86
|
+
options,
|
|
84
87
|
retry: {
|
|
85
88
|
// A failed first attempt may have allocated a sandbox whose id never reached this run;
|
|
86
89
|
// its own kill-on-timeout reclaims it.
|
|
@@ -418,7 +421,7 @@ export class LiveTerminalSandbox {
|
|
|
418
421
|
// --- Proven cleanup, by exact id, never Sandbox.list. ---
|
|
419
422
|
this.cleanup = await teardownSandbox({
|
|
420
423
|
allocation: this.allocation,
|
|
421
|
-
|
|
424
|
+
account: this.account,
|
|
422
425
|
...(this.startupCleanup === undefined ? {} : { startupCleanup: this.startupCleanup }),
|
|
423
426
|
startupCleanupDetail: this.startupCleanupDetail,
|
|
424
427
|
requestTimeoutMs,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { OwnedDesktopAllocation } from "../../substrates/desktop-session.js";
|
|
2
|
-
import {
|
|
2
|
+
import type { E2BAccount } from "../../substrates/e2b/connection.js";
|
|
3
|
+
import { E2BDesktopStartupError } from "../../substrates/e2b/sdk.js";
|
|
3
4
|
import type { TerminalLedgers } from "./types.js";
|
|
4
5
|
/**
|
|
5
6
|
* Tear the sandbox down and prove it by exact id, never by Sandbox.list (humanish must never
|
|
@@ -14,8 +15,8 @@ import type { TerminalLedgers } from "./types.js";
|
|
|
14
15
|
export declare function teardownSandbox(args: {
|
|
15
16
|
/** The allocation from acquisition; its release kills the exact id it captured. */
|
|
16
17
|
allocation: OwnedDesktopAllocation | undefined;
|
|
17
|
-
/**
|
|
18
|
-
|
|
18
|
+
/** The account the create used, for the Sandbox.getInfo(id) re-check only. */
|
|
19
|
+
account: E2BAccount | undefined;
|
|
19
20
|
startupCleanup?: E2BDesktopStartupError["cleanup"];
|
|
20
21
|
/** Why the startup guard's cleanup is unconfirmed, when it knows. */
|
|
21
22
|
startupCleanupDetail?: string | undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { toErrorMessage } from "../../evidence/redaction.js";
|
|
2
|
-
import { E2BDesktopStartupError, isSandboxNotFoundError
|
|
2
|
+
import { E2BDesktopStartupError, isSandboxNotFoundError } from "../../substrates/e2b/sdk.js";
|
|
3
3
|
import { releaseUnavailableDetail } from "../../substrates/e2b/sandbox.js";
|
|
4
4
|
/**
|
|
5
5
|
* Tear the sandbox down and prove it by exact id, never by Sandbox.list (humanish must never
|
|
@@ -12,8 +12,8 @@ import { releaseUnavailableDetail } from "../../substrates/e2b/sandbox.js";
|
|
|
12
12
|
* fails closed on an unproven teardown.
|
|
13
13
|
*/
|
|
14
14
|
export async function teardownSandbox(args) {
|
|
15
|
-
const { allocation,
|
|
16
|
-
if (allocation === undefined ||
|
|
15
|
+
const { allocation, account, startupCleanup, startupCleanupDetail, requestTimeoutMs, sanitize, recordLifecycle, warnings, } = args;
|
|
16
|
+
if (allocation === undefined || account === undefined) {
|
|
17
17
|
// create() can reject after its constructor acquired a handle. The default loader retains
|
|
18
18
|
// that authority and reclaims it before rejecting; the route itself never receives its ID.
|
|
19
19
|
if (startupCleanup === "killed" || startupCleanup === "already_gone") {
|
|
@@ -58,7 +58,7 @@ export async function teardownSandbox(args) {
|
|
|
58
58
|
const killNote = released.reason === "terminated"
|
|
59
59
|
? "kill(id) returned true (found and killed)"
|
|
60
60
|
: "kill(id) found the exact sandbox already gone (404)";
|
|
61
|
-
if (
|
|
61
|
+
if (account.getInfo === undefined) {
|
|
62
62
|
recordLifecycle("terminal-lab.cleanup.killed", `Sandbox ${sandboxId} reclaimed: ${killNote}; the installed SDK has no getInfo(id) to re-verify, so kill(id)'s own result is the proof.`);
|
|
63
63
|
return {
|
|
64
64
|
killed: true,
|
|
@@ -67,7 +67,7 @@ export async function teardownSandbox(args) {
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
try {
|
|
70
|
-
const info = await
|
|
70
|
+
const info = await account.getInfo(sandboxId, { requestTimeoutMs });
|
|
71
71
|
const state = info.state ?? "unknown";
|
|
72
72
|
recordLifecycle("terminal-lab.cleanup.unconfirmed", `Sandbox ${sandboxId} ${killNote}, but getInfo(id) still reports state=${state} (not confirmed reclaimed by id).`);
|
|
73
73
|
return {
|
package/dist/study/config.js
CHANGED
|
@@ -40,7 +40,7 @@ import { isComputerUseComposition, isSharedWorldComposition, routeOf } from "./r
|
|
|
40
40
|
import { declaredParticipantIds } from "./plan-participants.js";
|
|
41
41
|
import { declaredParticipantCount, participantList } from "./study-fields.js";
|
|
42
42
|
import { ID_PATTERN, STUDY_SCHEMA, } from "./types.js";
|
|
43
|
-
import { automaticAnalysisRouteReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, smtpValidationReason, taskProtocolValidationReason, } from "./validation.js";
|
|
43
|
+
import { automaticAnalysisRouteReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, smtpValidationReason, taskProtocolValidationReason, waitLimitValidationReason, } from "./validation.js";
|
|
44
44
|
import { forwardDeclaredWarnings, inertFieldLabels, inertFieldPaths } from "./warnings.js";
|
|
45
45
|
import { plural } from "../run/text.js";
|
|
46
46
|
/**
|
|
@@ -228,6 +228,9 @@ function checkStudyConfig(config) {
|
|
|
228
228
|
const outputLimitReason = outputTokenLimitValidationReason(config);
|
|
229
229
|
if (outputLimitReason)
|
|
230
230
|
return invalid(outputLimitReason);
|
|
231
|
+
const waitLimitReason = waitLimitValidationReason(config);
|
|
232
|
+
if (waitLimitReason)
|
|
233
|
+
return invalid(waitLimitReason);
|
|
231
234
|
// All-parallel default: a multi-participant study that does not declare execution.concurrency
|
|
232
235
|
// runs every participant at once; the declared field is a cap the author chose, never a mode.
|
|
233
236
|
// Independent computer-use participants resolve that default from the final participant count
|
package/dist/study/keys.d.ts
CHANGED
package/dist/study/keys.js
CHANGED
package/dist/study/migrate/v2.js
CHANGED
|
@@ -95,7 +95,8 @@ const V2_KEYS = {
|
|
|
95
95
|
lanes: PARTICIPANT_ENTRY,
|
|
96
96
|
// Roster groups are participants with a count; the parser expanded them into `lanes[]`.
|
|
97
97
|
roster: { ...PARTICIPANT_ENTRY, count: true },
|
|
98
|
-
|
|
98
|
+
// maxWaitMs and idleWaitMs arrived after v2, so a v2 file cannot carry them.
|
|
99
|
+
...without(ACTOR, "type", "maxWaitMs", "idleWaitMs"),
|
|
99
100
|
laneFocus: { id: true, label: true, instruction: true },
|
|
100
101
|
},
|
|
101
102
|
execution: withAfter(EXECUTION, "egressAllow", { caps: CAPS }),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DEVICE_PRESET_NAMES, isDevicePresetName } from "../device-presets.js";
|
|
2
2
|
import { isReasoningEffort, reasoningEffortNames } from "../../actors/reasoning-effort.js";
|
|
3
3
|
import { isMaxOutputTokens } from "../../actors/output-token-limit.js";
|
|
4
|
+
import { waitFieldsReason } from "../../actors/computer-use/wait.js";
|
|
4
5
|
import { isHttpUrl } from "./subject.js";
|
|
5
6
|
import { entryCredentialReason, urlCredentialReason } from "./url-credentials.js";
|
|
6
7
|
import { registeredComputerUseActors, registeredScriptedBrowserActors, registeredTerminalActors, } from "../routing.js";
|
|
@@ -54,6 +55,13 @@ export function parseActorFields(entry, path) {
|
|
|
54
55
|
return invalid(`${path}.maxOutputTokens must be a positive safe integer.`);
|
|
55
56
|
actor.maxOutputTokens = entry.maxOutputTokens;
|
|
56
57
|
}
|
|
58
|
+
const waitReason = waitFieldsReason(entry, `${path}.`);
|
|
59
|
+
if (waitReason)
|
|
60
|
+
return invalid(waitReason);
|
|
61
|
+
if (typeof entry.maxWaitMs === "number")
|
|
62
|
+
actor.maxWaitMs = entry.maxWaitMs;
|
|
63
|
+
if (typeof entry.idleWaitMs === "number")
|
|
64
|
+
actor.idleWaitMs = entry.idleWaitMs;
|
|
57
65
|
const localAgent = str(entry.localAgent);
|
|
58
66
|
if (entry.localAgent !== undefined) {
|
|
59
67
|
if (localAgent !== "codex" && localAgent !== "claude") {
|
|
@@ -36,6 +36,10 @@ interface DesktopParticipant extends Participant {
|
|
|
36
36
|
readonly dwell?: DwellWindow;
|
|
37
37
|
readonly reasoningEffort?: ReasoningEffort;
|
|
38
38
|
readonly maxOutputTokens?: number;
|
|
39
|
+
/** actor.maxWaitMs: the longest one wait action lasts. */
|
|
40
|
+
readonly maxWaitMs?: number;
|
|
41
|
+
/** actor.idleWaitMs: how long a wait with no duration lasts while the participant only waits. */
|
|
42
|
+
readonly idleWaitMs?: number;
|
|
39
43
|
};
|
|
40
44
|
}
|
|
41
45
|
/** Only independent computer-use participants consume a task protocol and their own target. */
|
|
@@ -12,6 +12,8 @@ function desktopParticipant(config, entry, index, kind, focus) {
|
|
|
12
12
|
const dwell = entry?.dwell ?? actor?.dwell;
|
|
13
13
|
const reasoningEffort = entry?.reasoningEffort ?? actor?.reasoningEffort;
|
|
14
14
|
const maxOutputTokens = actor?.maxOutputTokens;
|
|
15
|
+
const maxWaitMs = actor?.maxWaitMs;
|
|
16
|
+
const idleWaitMs = actor?.idleWaitMs;
|
|
15
17
|
return {
|
|
16
18
|
id: participantIdAt(index, entry?.id, kind),
|
|
17
19
|
index,
|
|
@@ -31,6 +33,8 @@ function desktopParticipant(config, entry, index, kind, focus) {
|
|
|
31
33
|
...(dwell === undefined ? {} : { dwell }),
|
|
32
34
|
...(reasoningEffort === undefined ? {} : { reasoningEffort }),
|
|
33
35
|
...(maxOutputTokens === undefined ? {} : { maxOutputTokens }),
|
|
36
|
+
...(maxWaitMs === undefined ? {} : { maxWaitMs }),
|
|
37
|
+
...(idleWaitMs === undefined ? {} : { idleWaitMs }),
|
|
34
38
|
},
|
|
35
39
|
};
|
|
36
40
|
}
|
package/dist/study/types.d.ts
CHANGED
|
@@ -349,6 +349,20 @@ export interface StudyActor {
|
|
|
349
349
|
model?: string;
|
|
350
350
|
/** First-party OpenAI CUA only: per-response output limit including reasoning, not a dollar cap. */
|
|
351
351
|
maxOutputTokens?: number;
|
|
352
|
+
/**
|
|
353
|
+
* Computer-use and shared-world: the longest one `wait` action lasts, in whole milliseconds from
|
|
354
|
+
* 1000 to 600000. Default 120000, or 30000 on a desktop with speech. A longer wait is shortened
|
|
355
|
+
* to it, the trace records a `wait shortened` notice, and the participant is told on its next
|
|
356
|
+
* turn.
|
|
357
|
+
*/
|
|
358
|
+
maxWaitMs?: number;
|
|
359
|
+
/**
|
|
360
|
+
* Computer-use and shared-world: how long a `wait` that names no duration lasts when the
|
|
361
|
+
* participant's turn only waits or takes screenshots, in whole milliseconds from 1000 to 600000
|
|
362
|
+
* and no longer than maxWaitMs. Default 5000. After an action in the same turn such a wait lasts
|
|
363
|
+
* 500 ms. OpenAI's computer-use `wait` never names a duration.
|
|
364
|
+
*/
|
|
365
|
+
idleWaitMs?: number;
|
|
352
366
|
/**
|
|
353
367
|
* `local-agent` only: which locally signed-in coding agent is the brain. Absent = codex.
|
|
354
368
|
*
|
|
@@ -36,6 +36,11 @@ export declare function taskProtocolValidationReason(config: StudyConfig, suppor
|
|
|
36
36
|
export declare function cloneTargetValidationReason(config: StudyConfig): string | null;
|
|
37
37
|
/** Refuse a claimed output bound when the route cannot pass it to the first-party provider. */
|
|
38
38
|
export declare function outputTokenLimitValidationReason(config: StudyConfig): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Refuse a wait field out of range, or an idle wait longer than the longest wait. On a route whose
|
|
41
|
+
* participants take no wait actions the fields are inert (warnings.ts).
|
|
42
|
+
*/
|
|
43
|
+
export declare function waitLimitValidationReason(config: StudyConfig): string | null;
|
|
39
44
|
/**
|
|
40
45
|
* Cross-validate a concurrent shared-world declaration. Returns the failure message,
|
|
41
46
|
* or null when valid. Includes the base shared-world checks plus the concurrent extras: a synthetic
|
package/dist/study/validation.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isMaxOutputTokens } from "../actors/output-token-limit.js";
|
|
2
|
+
import { waitFieldsReason } from "../actors/computer-use/wait.js";
|
|
2
3
|
import { PARTICIPANT_ID_MAX_CHARS, PARTICIPANT_ID_PATTERN } from "./parse/actors.js";
|
|
3
4
|
import { isHttpUrl, isLoopbackUrl } from "./parse/subject.js";
|
|
4
5
|
import { declaredTargets } from "./plan-participants.js";
|
|
@@ -235,6 +236,13 @@ export function outputTokenLimitValidationReason(config) {
|
|
|
235
236
|
}
|
|
236
237
|
return null;
|
|
237
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Refuse a wait field out of range, or an idle wait longer than the longest wait. On a route whose
|
|
241
|
+
* participants take no wait actions the fields are inert (warnings.ts).
|
|
242
|
+
*/
|
|
243
|
+
export function waitLimitValidationReason(config) {
|
|
244
|
+
return config.actor === undefined ? null : waitFieldsReason(config.actor, "actor.");
|
|
245
|
+
}
|
|
238
246
|
/**
|
|
239
247
|
* Shared-world participants share one live app, so at least two must be live at once. The
|
|
240
248
|
* sequential shared-world route (`execution.concurrency: 1`) was removed in 0.106.0; the parser
|
package/dist/study/warnings.js
CHANGED
|
@@ -50,6 +50,18 @@ const ACTOR_ROWS = [
|
|
|
50
50
|
field: "actor.model",
|
|
51
51
|
applies: (config, routes) => otherRoute(routes) && Boolean(config.actor?.model),
|
|
52
52
|
},
|
|
53
|
+
// Only a computer-use loop dispatches wait actions; terminal, scripted and preview participants
|
|
54
|
+
// take none.
|
|
55
|
+
{
|
|
56
|
+
field: "actor.maxWaitMs",
|
|
57
|
+
reason: "the longest wait action of a computer-use participant; needs route: computer-use or shared-world",
|
|
58
|
+
applies: (config, routes) => config.actor?.maxWaitMs !== undefined && !routes.cua && !routes.shared,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
field: "actor.idleWaitMs",
|
|
62
|
+
reason: "how long a computer-use participant's wait with no duration lasts; needs route: computer-use or shared-world",
|
|
63
|
+
applies: (config, routes) => config.actor?.idleWaitMs !== undefined && !routes.cua && !routes.shared,
|
|
64
|
+
},
|
|
53
65
|
];
|
|
54
66
|
const TERMINAL_ONLY = "needs subject.source: terminal-product + a registered terminal actor";
|
|
55
67
|
const RESERVED = "reserved; no route reads it yet";
|
package/dist/subject/serve.d.ts
CHANGED
|
@@ -37,3 +37,6 @@ export declare function runSubjectServePipeline(shell: Shell, args: {
|
|
|
37
37
|
* an in-sandbox git refresh, because the archive excludes .git). */
|
|
38
38
|
onPhaseComplete?: () => Promise<void>;
|
|
39
39
|
} & DetachedTimers): Promise<void>;
|
|
40
|
+
/** A serve.build that exited non-zero or ran out of time; the message carries its redacted log tail. */
|
|
41
|
+
export declare class SubjectBuildError extends Error {
|
|
42
|
+
}
|
package/dist/subject/serve.js
CHANGED
|
@@ -196,6 +196,9 @@ async function installSubject(step, command) {
|
|
|
196
196
|
throw new Error(`${headline}: ${failureTail(step.scrub(install.logTail))}`);
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
|
+
/** A serve.build that exited non-zero or ran out of time; the message carries its redacted log tail. */
|
|
200
|
+
export class SubjectBuildError extends Error {
|
|
201
|
+
}
|
|
199
202
|
/** serve.build, once. */
|
|
200
203
|
async function buildSubject(step, command) {
|
|
201
204
|
const { now } = step;
|
|
@@ -211,7 +214,7 @@ async function buildSubject(step, command) {
|
|
|
211
214
|
});
|
|
212
215
|
emitPhaseCompleted(step.onPhase, now, buildStartedAt, "build", build.ok, build.ok ? "subject build complete" : "subject build failed");
|
|
213
216
|
if (!build.ok) {
|
|
214
|
-
throw new
|
|
217
|
+
throw new SubjectBuildError(`subject build ${build.timedOut ? "timed out" : `failed (exit ${build.exitCode})`}: ${failureTail(step.scrub(build.logTail))}`);
|
|
215
218
|
}
|
|
216
219
|
}
|
|
217
220
|
/** serve.start as a detached process that keeps running. */
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
//
|
|
4
4
|
// - Scripts are written via `writeFile`, never heredocs, which eliminates the
|
|
5
5
|
// sentinel-collision bug class (a command line that equals the heredoc terminator) by
|
|
6
|
-
// construction.
|
|
6
|
+
// construction. The author's command is a script file of its own, so bash parses it apart
|
|
7
|
+
// from the wrapper: a syntax error in it is that script's exit 2, logged, and the wrapper
|
|
8
|
+
// still writes the status.
|
|
7
9
|
// - Bounded steps (install/build) run detached with an atomically-written status file
|
|
8
10
|
// (write tmp + mv), polled by short foreground commands; a timeout kills the process
|
|
9
11
|
// group and surfaces a capped log tail for the caller to redact and persist.
|
|
@@ -34,10 +36,11 @@ function assertName(name) {
|
|
|
34
36
|
function stepDir(name) {
|
|
35
37
|
return `${WORK_ROOT}/${name}`;
|
|
36
38
|
}
|
|
37
|
-
// The wrapper script: runs the command from its own session (Shell.start makes the
|
|
38
|
-
// process-group leader, so `kill -- -PID` reclaims the whole tree), logs everything,
|
|
39
|
-
// writes the exit code atomically so a poller can never read a half-written status.
|
|
40
|
-
|
|
39
|
+
// The wrapper script: runs the command script from its own session (Shell.start makes the
|
|
40
|
+
// wrapper the process-group leader, so `kill -- -PID` reclaims the whole tree), logs everything,
|
|
41
|
+
// and writes the exit code atomically so a poller can never read a half-written status. It holds
|
|
42
|
+
// no author text, so it always parses and always reaches the status write.
|
|
43
|
+
function wrapperScript(name, cwd) {
|
|
41
44
|
const dir = stepDir(name);
|
|
42
45
|
return [
|
|
43
46
|
"#!/bin/bash",
|
|
@@ -46,7 +49,7 @@ function wrapperScript(name, command, cwd) {
|
|
|
46
49
|
cwd === undefined
|
|
47
50
|
? ": # no cwd override"
|
|
48
51
|
: `cd ${shellQuote(cwd)} || { echo 127 > ${shellQuote(`${dir}/status.tmp`)}; mv ${shellQuote(`${dir}/status.tmp`)} ${shellQuote(`${dir}/status`)}; exit 127; }`,
|
|
49
|
-
`
|
|
52
|
+
`bash ${shellQuote(`${dir}/command.sh`)} > ${shellQuote(`${dir}/log.txt`)} 2>&1`,
|
|
50
53
|
"code=$?",
|
|
51
54
|
`echo $code > ${shellQuote(`${dir}/status.tmp`)}`,
|
|
52
55
|
`mv ${shellQuote(`${dir}/status.tmp`)} ${shellQuote(`${dir}/status`)}`,
|
|
@@ -59,7 +62,11 @@ async function writeAndLaunch(shell, name, command, cwd, requestTimeoutMs) {
|
|
|
59
62
|
const dir = stepDir(name);
|
|
60
63
|
const scriptPath = `${dir}/run.sh`;
|
|
61
64
|
await runOrThrow(shell, `mkdir -p ${shellQuote(dir)}`, { requestTimeoutMs });
|
|
62
|
-
|
|
65
|
+
// The study parser trims each command, so a YAML `|` block arrives without its final newline.
|
|
66
|
+
// bash closes a heredoc whose delimiter is a script's last line either way; the newline makes
|
|
67
|
+
// the file plain text.
|
|
68
|
+
await shell.writeFile(`${dir}/command.sh`, `${command}\n`);
|
|
69
|
+
await shell.writeFile(scriptPath, wrapperScript(name, cwd));
|
|
63
70
|
throwOnExit(await shell.start(`bash ${shellQuote(scriptPath)}`, { requestTimeoutMs }));
|
|
64
71
|
}
|
|
65
72
|
/** Read the capped log tail for a step (raw; caller redacts). */
|
|
@@ -54,8 +54,6 @@ export interface E2BDesktopLike {
|
|
|
54
54
|
wait(ms: number): Promise<void> | void;
|
|
55
55
|
}
|
|
56
56
|
export interface E2BDesktopExecutorOptions {
|
|
57
|
-
/** Fallback wait when a wait action carries no ms. Default 500. */
|
|
58
|
-
defaultWaitMs?: number;
|
|
59
57
|
/**
|
|
60
58
|
* Pixels of CuaAction scroll dy per one SDK scroll tick. The executor maps
|
|
61
59
|
* abs(dy) / scrollAmountPerTick to the SDK's integer `amount` (floored at 1 for
|
|
@@ -2,8 +2,8 @@ import { shellQuote } from "../shell.js";
|
|
|
2
2
|
import { perceptualSignature } from "../../evidence/frame-signature.js";
|
|
3
3
|
import { commandFailureInfo } from "../command-failure.js";
|
|
4
4
|
import { ComputerUseExecutorError } from "../../actors/computer-use/executor-error.js";
|
|
5
|
+
import { CUA_WAIT_LIMITS } from "../../actors/computer-use/wait.js";
|
|
5
6
|
import { xdotoolChord, xdotoolHeldModifiers } from "../../guest/desktop-keys.js";
|
|
6
|
-
const DEFAULT_WAIT_MS = 500;
|
|
7
7
|
const DEFAULT_SCROLL_AMOUNT_PER_TICK = 100;
|
|
8
8
|
const TYPE_COMMAND_TIMEOUT_MS = 15_000;
|
|
9
9
|
const KEY_COMMAND_TIMEOUT_MS = 15_000;
|
|
@@ -145,7 +145,6 @@ async function typeText(desktop, text) {
|
|
|
145
145
|
* screenshot or action is ever logged.
|
|
146
146
|
*/
|
|
147
147
|
export function createE2BDesktopExecutor(desktop, options = {}) {
|
|
148
|
-
const defaultWaitMs = options.defaultWaitMs ?? DEFAULT_WAIT_MS;
|
|
149
148
|
const scrollAmountPerTick = options.scrollAmountPerTick ?? DEFAULT_SCROLL_AMOUNT_PER_TICK;
|
|
150
149
|
return {
|
|
151
150
|
async observe() {
|
|
@@ -258,7 +257,8 @@ export function createE2BDesktopExecutor(desktop, options = {}) {
|
|
|
258
257
|
return;
|
|
259
258
|
}
|
|
260
259
|
case "wait":
|
|
261
|
-
|
|
260
|
+
// The loop sends every wait with its length; planWait owns the lengths.
|
|
261
|
+
await desktop.wait(action.ms ?? CUA_WAIT_LIMITS.settleMs);
|
|
262
262
|
return;
|
|
263
263
|
case "screenshot":
|
|
264
264
|
// No-op: the loop calls observe() separately to capture each frame, so
|