humanish 0.125.0 → 0.127.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/actors/computer-use/actor.d.ts +2 -0
- package/dist/actors/computer-use/actor.js +1 -0
- package/dist/actors/computer-use/loop/clock.d.ts +9 -8
- package/dist/actors/computer-use/loop/clock.js +27 -17
- package/dist/actors/computer-use/loop/debrief.d.ts +1 -1
- package/dist/actors/computer-use/loop/debrief.js +15 -5
- package/dist/actors/computer-use/loop/ending.d.ts +10 -5
- package/dist/actors/computer-use/loop/ending.js +12 -7
- package/dist/actors/computer-use/loop/observation.d.ts +6 -0
- package/dist/actors/computer-use/loop/observation.js +19 -1
- package/dist/actors/computer-use/loop/session.d.ts +4 -2
- package/dist/actors/computer-use/loop/session.js +4 -1
- package/dist/actors/computer-use/loop/trace.js +4 -2
- package/dist/actors/computer-use/loop/types.d.ts +22 -8
- package/dist/actors/computer-use/loop.js +5 -5
- package/dist/actors/computer-use/openai-provider.js +1 -1
- package/dist/actors/contract.d.ts +31 -8
- package/dist/actors/contract.js +3 -0
- package/dist/actors/goal-source.d.ts +32 -2
- package/dist/actors/goal-source.js +41 -2
- package/dist/actors/stop-cause.js +1 -0
- package/dist/analysis/evidence-sources.js +2 -2
- package/dist/analysis/types.d.ts +2 -2
- package/dist/analysis/validation.js +2 -1
- package/dist/cli/commands/study.js +8 -0
- package/dist/evidence/redaction.d.ts +1 -0
- package/dist/evidence/redaction.js +1 -0
- package/dist/evidence/trace-page.d.ts +6 -0
- package/dist/evidence/trace-page.js +90 -0
- package/dist/observer/data.js +8 -5
- package/dist/observer/feed-cursors.d.ts +14 -0
- package/dist/observer/feed-cursors.js +61 -0
- package/dist/observer/participant-items.d.ts +7 -0
- package/dist/observer/pinned-files.d.ts +2 -1
- package/dist/observer/pinned-files.js +2 -2
- package/dist/observer/run-routes.d.ts +1 -1
- package/dist/observer/run-routes.js +38 -17
- package/dist/observer-app.html +7 -7
- package/dist/routes/computer-use/bundle.js +4 -0
- package/dist/routes/computer-use/diagnostics.d.ts +1 -1
- package/dist/routes/computer-use/diagnostics.js +3 -3
- package/dist/routes/computer-use/e2b-desktop/desktop.js +2 -1
- package/dist/routes/computer-use/e2b-desktop/prepare.d.ts +6 -0
- package/dist/routes/computer-use/e2b-desktop/prepare.js +47 -1
- package/dist/routes/computer-use/e2b-desktop/start.d.ts +2 -1
- package/dist/routes/computer-use/e2b-desktop/start.js +15 -3
- package/dist/routes/computer-use/e2b-desktop/state.d.ts +3 -1
- package/dist/routes/computer-use/e2b-desktop/state.js +2 -0
- package/dist/routes/computer-use/fanout-bundle.js +3 -0
- package/dist/routes/computer-use/fanout-records.js +5 -1
- package/dist/routes/computer-use/participant-deps.d.ts +1 -1
- package/dist/routes/computer-use/participant-desktop.d.ts +1 -1
- package/dist/routes/computer-use/participant-execution.js +6 -2
- package/dist/routes/computer-use/participant-facts.js +4 -1
- package/dist/routes/computer-use/participant-model.js +16 -6
- package/dist/routes/computer-use/participant-runs.d.ts +10 -10
- package/dist/routes/computer-use/participant-runs.js +54 -84
- package/dist/routes/computer-use/plan.d.ts +2 -0
- package/dist/routes/computer-use/plan.js +65 -17
- package/dist/routes/computer-use/rerun-selection.js +8 -5
- package/dist/routes/computer-use/setup.d.ts +1 -0
- package/dist/routes/computer-use/setup.js +29 -1
- package/dist/routes/computer-use/single-bundle.d.ts +5 -1
- package/dist/routes/computer-use/single-bundle.js +11 -1
- package/dist/routes/computer-use/types.d.ts +18 -13
- package/dist/routes/computer-use/types.js +0 -3
- package/dist/routes/shared-world/bundle-records.js +4 -1
- package/dist/routes/shared-world/external-public.js +1 -1
- package/dist/routes/shared-world/participant-specs.js +3 -2
- package/dist/routes/shared-world/plan.d.ts +2 -0
- package/dist/routes/shared-world/plan.js +8 -3
- package/dist/routes/shared-world/provisioned.js +1 -1
- package/dist/run/bundle.d.ts +11 -1
- package/dist/run/contained-output.d.ts +7 -3
- package/dist/run/contained-output.js +11 -7
- package/dist/run/judge.d.ts +14 -4
- package/dist/run/judge.js +17 -12
- package/dist/run/live-items.d.ts +19 -0
- package/dist/run/live-items.js +28 -8
- package/dist/run/notes.js +1 -1
- package/dist/run/outcomes.d.ts +8 -2
- package/dist/run/outcomes.js +56 -14
- package/dist/run/stream-trace.d.ts +25 -4
- package/dist/run/stream-trace.js +55 -17
- package/dist/run/streams.d.ts +16 -0
- package/dist/study/arrivals.d.ts +105 -34
- package/dist/study/arrivals.js +179 -74
- package/dist/study/bundle-estimate.d.ts +12 -10
- package/dist/study/bundle-estimate.js +138 -72
- package/dist/study/desktop-files.d.ts +37 -0
- package/dist/study/desktop-files.js +174 -0
- package/dist/study/keys.d.ts +8 -0
- package/dist/study/keys.js +3 -0
- package/dist/study/migrate/v2.js +12 -3
- package/dist/study/parse/actors.d.ts +1 -1
- package/dist/study/parse/actors.js +28 -20
- package/dist/study/parse/clock.d.ts +18 -0
- package/dist/study/parse/clock.js +36 -0
- package/dist/study/plan-participants.d.ts +19 -10
- package/dist/study/plan-participants.js +13 -9
- package/dist/study/plan-types.d.ts +6 -1
- package/dist/study/plan.d.ts +2 -1
- package/dist/study/plan.js +28 -15
- package/dist/study/preflight.js +4 -0
- package/dist/study/types.d.ts +35 -5
- package/dist/study/validation.js +3 -0
- package/dist/study/warnings.d.ts +2 -2
- package/dist/study/warnings.js +7 -0
- package/dist/subject/local-tree-archive.d.ts +7 -0
- package/dist/subject/local-tree-archive.js +12 -0
- package/dist/substrates/e2b/desktop-browser.d.ts +4 -1
- package/dist/substrates/e2b/desktop-browser.js +6 -2
- package/dist/substrates/e2b/desktop-clock.d.ts +10 -0
- package/dist/substrates/e2b/desktop-clock.js +29 -0
- package/dist/substrates/e2b/lifetime.d.ts +22 -8
- package/dist/substrates/e2b/lifetime.js +54 -10
- package/dist/substrates/e2b/sdk.d.ts +6 -0
- package/dist/verify/pages.d.ts +7 -0
- package/dist/verify/pages.js +50 -0
- package/dist/verify/verify.js +10 -3
- package/docs/architecture/actor-contract.md +2 -0
- package/docs/architecture/external-public-shared-world.md +4 -1
- package/docs/architecture/observer.md +10 -1
- package/docs/contracts/policy.md +7 -0
- package/docs/contracts/run-bundle.md +31 -1
- package/docs/contracts/schemas.md +93 -11
- package/package.json +2 -2
package/dist/study/arrivals.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// When each participant of a computer-use or shared-world run starts, and on computer use when it
|
|
2
2
|
// ends. A study may give each participant an offset from the moment the run starts its participants
|
|
3
3
|
// (`participants[].startAfterMs`) and, on computer use, an end on the same clock
|
|
4
|
-
// (`participants[].endAfterMs`)
|
|
5
|
-
// participant the time left (`windDownMs`).
|
|
6
|
-
//
|
|
4
|
+
// (`participants[].endAfterMs`) or a cap on its own session (`maxSessionMs`), with a wind-down
|
|
5
|
+
// before either in which each turn tells the participant the time left (`windDownMs`). Each
|
|
6
|
+
// participant's ParticipantSchedule resolves those fields once; everything that reads a
|
|
7
|
+
// participant's time reads the schedule. The plan simulates the start queue to report the schedule
|
|
8
|
+
// and when the last session ends at the latest; the routes start their participants through
|
|
7
9
|
// runOnSchedule, which waits for each one's time and then for a free slot. A participant's desktop
|
|
8
10
|
// is created when it starts, so a late start holds no sandbox before then.
|
|
9
11
|
import { setTimeout as delay } from "node:timers/promises";
|
|
@@ -22,8 +24,8 @@ export function isStartAfterMs(value) {
|
|
|
22
24
|
value >= 0 &&
|
|
23
25
|
value <= LATEST_START_AFTER_MS);
|
|
24
26
|
}
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
+
/** The longest default wind-down: 10 minutes is enough to wrap up what a participant is doing. */
|
|
28
|
+
const MAX_DEFAULT_WIND_DOWN_MS = 10 * 60_000;
|
|
27
29
|
/**
|
|
28
30
|
* Room a scheduled participant's session keeps past its end for the turn in flight and the closing
|
|
29
31
|
* report. The loop ends the session at the end; this keeps the session's own time limit, and the
|
|
@@ -32,13 +34,18 @@ export const DEFAULT_WIND_DOWN_MS = 10 * 60_000;
|
|
|
32
34
|
const SCHEDULED_END_ROOM_MS = 5 * 60_000;
|
|
33
35
|
/**
|
|
34
36
|
* Why a participant's declared end cannot run, or undefined: an end that is not a whole number of
|
|
35
|
-
* milliseconds up to 24 hours, one at or before its start,
|
|
36
|
-
*
|
|
37
|
+
* milliseconds up to 24 hours, one at or before its start, a cap that is not a whole number of
|
|
38
|
+
* milliseconds from 1 to 24 hours, a cap beside an end, or a wind-down that is not a whole number
|
|
39
|
+
* of milliseconds or has neither. `path` names the entry, as in "participants[1]".
|
|
37
40
|
*/
|
|
38
41
|
export function windowReason(entry, path) {
|
|
39
|
-
const { endAfterMs, windDownMs } = entry;
|
|
40
|
-
if (
|
|
41
|
-
return `${path}
|
|
42
|
+
const { endAfterMs, maxSessionMs, windDownMs } = entry;
|
|
43
|
+
if (maxSessionMs !== undefined && endAfterMs !== undefined)
|
|
44
|
+
return `${path} sets both endAfterMs and maxSessionMs. endAfterMs holds the participant until a set time, and maxSessionMs lets it end its session sooner, up to a cap, so a participant takes one of them. Keep endAfterMs for one that stays until a set time, or maxSessionMs for one that may leave when it is done.`;
|
|
45
|
+
if (maxSessionMs !== undefined && !(isStartAfterMs(maxSessionMs) && maxSessionMs > 0))
|
|
46
|
+
return `${path}.maxSessionMs must be a whole number of milliseconds from 1 to ${LATEST_START_AFTER_MS} (24 hours): the longest the participant's session lasts, counted from its start.`;
|
|
47
|
+
if (windDownMs !== undefined && endAfterMs === undefined && maxSessionMs === undefined)
|
|
48
|
+
return `${path}.windDownMs is how long before its endAfterMs or maxSessionMs a participant is told the time left, so it needs one of them. Add an end, or remove windDownMs.`;
|
|
42
49
|
if (windDownMs !== undefined && !isStartAfterMs(windDownMs))
|
|
43
50
|
return `${path}.windDownMs must be a whole number of milliseconds from 0 to ${LATEST_START_AFTER_MS} (24 hours): how long before its end the participant is told the time left.`;
|
|
44
51
|
if (endAfterMs === undefined)
|
|
@@ -53,58 +60,120 @@ export function windowReason(entry, path) {
|
|
|
53
60
|
return undefined;
|
|
54
61
|
}
|
|
55
62
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
63
|
+
* The schedule of a participants entry that windowReason accepts. A cap wins over an end, which
|
|
64
|
+
* windowReason refuses beside it anyway.
|
|
58
65
|
*/
|
|
59
|
-
export function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
export function participantSchedule(entry) {
|
|
67
|
+
const start = entry.startAfterMs === undefined ? {} : { startAfterMs: entry.startAfterMs };
|
|
68
|
+
if (entry.maxSessionMs !== undefined)
|
|
69
|
+
return {
|
|
70
|
+
...start,
|
|
71
|
+
end: { kind: "session_cap", ...endTiming(entry.maxSessionMs, entry.windDownMs) },
|
|
72
|
+
};
|
|
73
|
+
if (entry.endAfterMs === undefined)
|
|
74
|
+
return start;
|
|
75
|
+
const windowMs = entry.endAfterMs - (entry.startAfterMs ?? 0);
|
|
76
|
+
return {
|
|
77
|
+
...start,
|
|
78
|
+
end: {
|
|
79
|
+
kind: "scheduled_end",
|
|
80
|
+
endAfterMs: entry.endAfterMs,
|
|
81
|
+
...endTiming(windowMs, entry.windDownMs),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
63
84
|
}
|
|
64
85
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
86
|
+
* A window's wind-down and session budget. Without a declared `windDownMs` the wind-down is a
|
|
87
|
+
* quarter of the window, at most 10 minutes. A fixed 10 minutes covered the whole of a window of
|
|
88
|
+
* 10 minutes or less, so a short session was told its end from the first turn. A quarter leaves
|
|
89
|
+
* most of a short session without a countdown, and a window of 40 minutes or more keeps the 10
|
|
90
|
+
* minutes.
|
|
68
91
|
*/
|
|
69
|
-
|
|
70
|
-
if (participant.endAfterMs === undefined)
|
|
71
|
-
return undefined;
|
|
92
|
+
function endTiming(windowMs, windDownMs) {
|
|
72
93
|
return {
|
|
73
|
-
|
|
74
|
-
windDownMs:
|
|
94
|
+
windowMs,
|
|
95
|
+
windDownMs: windDownMs ?? Math.min(MAX_DEFAULT_WIND_DOWN_MS, Math.floor(windowMs / 4)),
|
|
96
|
+
sessionMs: windowMs + SCHEDULED_END_ROOM_MS,
|
|
75
97
|
};
|
|
76
98
|
}
|
|
77
99
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
|
|
100
|
+
* The schedule a rerun gives a participant: it starts with the rerun, and a scheduled end keeps its
|
|
101
|
+
* time on the rerun's clock and its wind-down, so its window runs from the rerun's start.
|
|
102
|
+
*/
|
|
103
|
+
export function startingWithTheRun(schedule) {
|
|
104
|
+
const { end } = schedule;
|
|
105
|
+
if (end === undefined)
|
|
106
|
+
return {};
|
|
107
|
+
if (end.kind === "session_cap")
|
|
108
|
+
return { end };
|
|
109
|
+
return participantSchedule({ endAfterMs: end.endAfterMs, windDownMs: end.windDownMs });
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A participant's own session budget and sandbox deadline. With an end or a cap it is the end's
|
|
113
|
+
* `sessionMs`, and the sandbox keeps the study sandbox's headroom over the study session on top;
|
|
114
|
+
* without one, the study's own.
|
|
81
115
|
*/
|
|
82
|
-
export function
|
|
116
|
+
export function participantSession(schedule, study) {
|
|
117
|
+
const sessionMs = schedule.end?.sessionMs ?? study.sessionMs;
|
|
118
|
+
return { sessionMs, sandboxMs: study.sandboxMs - study.sessionMs + sessionMs };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* How a participant that was due at `scheduledAt` (runOnSchedule's ScheduledStart) ends, or
|
|
122
|
+
* undefined without an end or a cap. A scheduled end comes its window after the participant's
|
|
123
|
+
* time, which is its `endAfterMs` on the run's clock, even when it waited for a slot; a cap is
|
|
124
|
+
* whole from wherever its session starts.
|
|
125
|
+
*/
|
|
126
|
+
export function participantEnd(schedule, scheduledAt) {
|
|
127
|
+
const { end } = schedule;
|
|
128
|
+
if (end === undefined)
|
|
129
|
+
return undefined;
|
|
130
|
+
if (end.kind === "session_cap")
|
|
131
|
+
return { kind: "session_cap", afterMs: end.windowMs, windDownMs: end.windDownMs };
|
|
132
|
+
return { kind: "scheduled_end", atMs: scheduledAt + end.windowMs, windDownMs: end.windDownMs };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Why the sandbox of the first participant whose window or cap passes `ceilingMs` cannot live that
|
|
136
|
+
* long, or undefined. Its deadline is its participantSession's, from the study's session budget and
|
|
137
|
+
* sandbox deadline.
|
|
138
|
+
*/
|
|
139
|
+
export function windowDeadlineRefusal(participants, study, ceilingMs) {
|
|
83
140
|
const inMinutes = (ms) => Math.round(ms / 60_000);
|
|
84
|
-
for (const
|
|
85
|
-
|
|
141
|
+
for (const { id, schedule } of participants) {
|
|
142
|
+
const { end } = schedule;
|
|
143
|
+
if (end === undefined)
|
|
86
144
|
continue;
|
|
87
|
-
const deadlineMs =
|
|
145
|
+
const deadlineMs = participantSession(schedule, study).sandboxMs;
|
|
88
146
|
if (deadlineMs <= ceilingMs)
|
|
89
147
|
continue;
|
|
90
|
-
const
|
|
91
|
-
|
|
148
|
+
const works = end.kind === "scheduled_end"
|
|
149
|
+
? `works from +${formatDuration(schedule.startAfterMs ?? 0)} to +${formatDuration(end.endAfterMs)}`
|
|
150
|
+
: `may work for up to ${formatDuration(end.windowMs)}`;
|
|
151
|
+
const window = end.kind === "scheduled_end" ? "window" : "cap";
|
|
152
|
+
return `Participant "${id}" ${works}, so its sandbox needs a ${inMinutes(deadlineMs)}m deadline: the ${window}, ${inMinutes(SCHEDULED_END_ROOM_MS)}m for its closing report and ${inMinutes(study.sandboxMs - study.sessionMs)}m to set up and tear down. A sandbox may not live longer than ${ceilingMs / 60_000}m. Shorten its ${window}.${ceilingAdvice(deadlineMs)}`;
|
|
92
153
|
}
|
|
93
154
|
return undefined;
|
|
94
155
|
}
|
|
95
156
|
/**
|
|
96
|
-
* Each participant's window for `study check`, when any participant declares an end: when
|
|
97
|
-
* and ends and when it is first told the time left,
|
|
157
|
+
* Each participant's window for `study check`, when any participant declares an end or a cap: when
|
|
158
|
+
* it starts and ends and when it is first told the time left, when it starts and its cap and how
|
|
159
|
+
* far into its session it is first told the time left, or when it starts and the study's
|
|
160
|
+
* `sessionMs`.
|
|
98
161
|
*/
|
|
99
162
|
export function describeWindows(participants, sessionMs) {
|
|
100
|
-
if (
|
|
163
|
+
if (participants.every((participant) => participant.schedule.end === undefined))
|
|
101
164
|
return undefined;
|
|
102
|
-
const windows = participants.map((
|
|
103
|
-
const
|
|
104
|
-
|
|
165
|
+
const windows = participants.map(({ id, schedule }) => {
|
|
166
|
+
const startAfterMs = schedule.startAfterMs ?? 0;
|
|
167
|
+
const start = `${id} from +${formatDuration(startAfterMs)}`;
|
|
168
|
+
const { end } = schedule;
|
|
169
|
+
if (end === undefined)
|
|
105
170
|
return `${start} for up to ${formatDuration(sessionMs)}`;
|
|
106
|
-
|
|
107
|
-
|
|
171
|
+
if (end.kind === "session_cap") {
|
|
172
|
+
const told = Math.max(0, end.windowMs - end.windDownMs);
|
|
173
|
+
return `${start} until it ends itself or for up to ${formatDuration(end.windowMs)}, told the time left from ${formatDuration(told)} into its session`;
|
|
174
|
+
}
|
|
175
|
+
const told = Math.max(startAfterMs, end.endAfterMs - end.windDownMs);
|
|
176
|
+
return `${start} to +${formatDuration(end.endAfterMs)}, told the time left from +${formatDuration(told)}`;
|
|
108
177
|
});
|
|
109
178
|
return `${windows.join("; ")}.`;
|
|
110
179
|
}
|
|
@@ -126,34 +195,42 @@ export function rosterStartsReason(roster) {
|
|
|
126
195
|
return undefined;
|
|
127
196
|
}
|
|
128
197
|
/**
|
|
129
|
-
* The schedule a roster's
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
198
|
+
* The schedule a roster's participants give under the queue. Participants start in schedule order,
|
|
199
|
+
* by offset and then roster position, each at its time or when a slot frees, whichever is later;
|
|
200
|
+
* this is the rule runOnSchedule follows. A participant with an end or a cap holds its slot for its
|
|
201
|
+
* own session budget, and the others for `queue.sessionMs`. A participant whose slot would free
|
|
202
|
+
* only at or after its scheduled end takes none: the run skips it (pastEndReason).
|
|
134
203
|
*/
|
|
135
|
-
export function planArrivals(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
204
|
+
export function planArrivals(schedules, queue) {
|
|
205
|
+
return simulateQueue(schedules, queue).arrivals;
|
|
206
|
+
}
|
|
207
|
+
/** planArrivals, and the roster indexes of the participants the run would skip. */
|
|
208
|
+
function simulateQueue(schedules, queue) {
|
|
209
|
+
const due = schedules.map((schedule) => schedule.startAfterMs ?? 0);
|
|
210
|
+
const sessionsMs = schedules.map((schedule) => schedule.end?.sessionMs ?? queue.sessionMs);
|
|
211
|
+
const session = (index) => sessionsMs[index];
|
|
212
|
+
const { starts, skipped } = queueStarts(schedules, due, queue, session);
|
|
213
|
+
const skips = new Set(skipped);
|
|
214
|
+
const started = starts.flatMap((_, index) => (skips.has(index) ? [] : [index]));
|
|
139
215
|
let waiting = 0;
|
|
140
216
|
let longestWaitMs = 0;
|
|
141
|
-
for (const
|
|
142
|
-
const waitMs =
|
|
217
|
+
for (const index of started) {
|
|
218
|
+
const waitMs = starts[index] - due[index];
|
|
143
219
|
if (waitMs > 0)
|
|
144
220
|
waiting += 1;
|
|
145
221
|
longestWaitMs = Math.max(longestWaitMs, waitMs);
|
|
146
222
|
}
|
|
147
|
-
|
|
148
|
-
declared:
|
|
223
|
+
const arrivals = {
|
|
224
|
+
declared: schedules.some((schedule) => schedule.startAfterMs !== undefined),
|
|
149
225
|
firstStartMs: Math.min(...due),
|
|
150
226
|
lastStartMs: Math.max(...due),
|
|
151
|
-
peak: mostAtOnce(starts, session),
|
|
227
|
+
peak: mostAtOnce(started.map((index) => [starts[index], session(index)])),
|
|
152
228
|
waiting,
|
|
153
229
|
longestWaitMs,
|
|
154
|
-
lastEndMs: Math.max(...
|
|
230
|
+
lastEndMs: Math.max(...started.map((index) => starts[index] + session(index))),
|
|
155
231
|
sessionMs: queue.sessionMs,
|
|
156
232
|
};
|
|
233
|
+
return { arrivals, skipped };
|
|
157
234
|
}
|
|
158
235
|
/**
|
|
159
236
|
* The plan's schedule, after a "schedule:" label: the first and last start, and how many run at
|
|
@@ -169,21 +246,43 @@ export function describeArrivals(arrivals) {
|
|
|
169
246
|
* A planner's arrival plan for its participants under the start queue, and the warnings the plan
|
|
170
247
|
* records about it. A participant's desktop is created when it starts, so on computer use a late
|
|
171
248
|
* start adds nothing to any sandbox's deadline; the provisioned shared world checks its app's
|
|
172
|
-
* sandbox with servedScheduleRefusal. `lowered` is
|
|
173
|
-
* roster in waves.
|
|
249
|
+
* sandbox with servedScheduleRefusal. `lowered` is participantsAtOnce's warning that the E2B plan's
|
|
250
|
+
* limit or HUMANISH_CUA_MAX_CONCURRENCY runs the roster in waves.
|
|
174
251
|
*/
|
|
175
252
|
export function planStarts(participants, queue, lowered) {
|
|
176
|
-
const arrivals =
|
|
177
|
-
|
|
253
|
+
const { arrivals, skipped } = simulateQueue(participants.map((participant) => participant.schedule), queue);
|
|
254
|
+
const ids = skipped.map((index) => participants[index].id);
|
|
255
|
+
return { arrivals, warnings: startQueueWarnings(arrivals, ids, queue, lowered) };
|
|
178
256
|
}
|
|
179
257
|
// A roster that starts together keeps `lowered`. A declared schedule spreads its participants
|
|
180
|
-
// itself, so it is warned only when it holds someone past their time
|
|
181
|
-
// when the limit is the reason.
|
|
182
|
-
function startQueueWarnings(arrivals, queue, lowered) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const late = `${plural(arrivals.waiting, "participant")} would start after their time
|
|
186
|
-
|
|
258
|
+
// itself, so it is warned only when it holds someone past their time or skips someone, with
|
|
259
|
+
// `lowered` beside that when the limit is the reason.
|
|
260
|
+
function startQueueWarnings(arrivals, skipped, queue, lowered) {
|
|
261
|
+
const budget = `when every session uses its ${formatDuration(arrivals.sessionMs)} budget`;
|
|
262
|
+
const atMost = `the run starts at most ${queue.slots} at once`;
|
|
263
|
+
const late = `${plural(arrivals.waiting, "participant")} would start after their time ${budget}, the latest by ${formatDuration(arrivals.longestWaitMs)}: ${atMost} and holds the next one until a slot frees.`;
|
|
264
|
+
const one = skipped.length === 1;
|
|
265
|
+
const names = skipped.map((id) => `"${id}"`);
|
|
266
|
+
const who = one
|
|
267
|
+
? `Participant ${names[0]}`
|
|
268
|
+
: `${skipped.length} participants (${names.join(", ")})`;
|
|
269
|
+
const skip = `${who} would still be waiting for a free slot at ${one ? "its" : "their"} end (participants[].endAfterMs) ${budget}, so the run would skip ${one ? "it without creating its desktop" : "them without creating their desktops"}: ${atMost}. Let more participants run at once, or give ${one ? "it" : "them"} a later end.`;
|
|
270
|
+
const held = arrivals.waiting > 0 || skipped.length > 0;
|
|
271
|
+
return [
|
|
272
|
+
...(arrivals.declared && arrivals.waiting > 0 ? [late] : []),
|
|
273
|
+
...(skipped.length > 0 ? [skip] : []),
|
|
274
|
+
...(lowered !== undefined && (!arrivals.declared || held) ? [lowered] : []),
|
|
275
|
+
];
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Why a participant whose start comes at `nowMs` is skipped, or undefined: its scheduled end came
|
|
279
|
+
* while it waited, so its session would end before it began. The plan's simulation applies the
|
|
280
|
+
* same rule (planArrivals).
|
|
281
|
+
*/
|
|
282
|
+
export function pastEndReason(end, nowMs) {
|
|
283
|
+
if (end?.kind !== "scheduled_end" || nowMs < end.atMs)
|
|
284
|
+
return undefined;
|
|
285
|
+
return "skipped: its scheduled end (participants[].endAfterMs) came while it waited to start, so its session would have ended before it began; no desktop was created";
|
|
187
286
|
}
|
|
188
287
|
/**
|
|
189
288
|
* Why a sandbox that serves the app until the last participant ends, the provisioned shared
|
|
@@ -217,8 +316,9 @@ function scheduleOrder(due) {
|
|
|
217
316
|
return due.map((_, index) => index).sort((a, b) => due[a] - due[b] || a - b);
|
|
218
317
|
}
|
|
219
318
|
/** Each participant's start in roster order, when each session holds its slot for `session(index)`. */
|
|
220
|
-
function queueStarts(due, queue, session) {
|
|
319
|
+
function queueStarts(schedules, due, queue, session) {
|
|
221
320
|
const starts = [...due];
|
|
321
|
+
const skipped = [];
|
|
222
322
|
const queued = scheduleOrder(due).filter((index) => index !== queue.ownSlot);
|
|
223
323
|
const pooled = queue.ownSlot === undefined ? queue.slots : queue.slots - 1;
|
|
224
324
|
// When each slot is next free; a participant takes the one that frees first.
|
|
@@ -227,16 +327,21 @@ function queueStarts(due, queue, session) {
|
|
|
227
327
|
const slot = free.indexOf(Math.min(...free));
|
|
228
328
|
const start = Math.max(due[index], free[slot]);
|
|
229
329
|
starts[index] = start;
|
|
230
|
-
|
|
330
|
+
const end = schedules[index]?.end;
|
|
331
|
+
if (end?.kind === "scheduled_end" && start >= end.endAfterMs)
|
|
332
|
+
skipped.push(index);
|
|
333
|
+
else
|
|
334
|
+
free[slot] = start + session(index);
|
|
231
335
|
}
|
|
232
|
-
return starts;
|
|
336
|
+
return { starts, skipped };
|
|
233
337
|
}
|
|
234
|
-
|
|
338
|
+
/** The most sessions running at once, each given as its start and how long it holds its slot. */
|
|
339
|
+
function mostAtOnce(sessions) {
|
|
235
340
|
// An end sorts before a start at the same instant: that slot is free again.
|
|
236
|
-
const changes =
|
|
237
|
-
.flatMap((start,
|
|
341
|
+
const changes = sessions
|
|
342
|
+
.flatMap(([start, ms]) => [
|
|
238
343
|
{ at: start, by: 1 },
|
|
239
|
-
{ at: start +
|
|
344
|
+
{ at: start + ms, by: -1 },
|
|
240
345
|
])
|
|
241
346
|
.sort((a, b) => a.at - b.at || a.by - b.by);
|
|
242
347
|
let live = 0;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { DwellWindow } from "../actors/stop-conditions.js";
|
|
2
|
-
|
|
2
|
+
import type { ParticipantSchedule } from "./arrivals.js";
|
|
3
|
+
/** One participant as the plan has it: its schedule and its dwell window. */
|
|
3
4
|
export interface EstimatedParticipant {
|
|
4
|
-
readonly
|
|
5
|
-
readonly endAfterMs?: number;
|
|
5
|
+
readonly schedule: ParticipantSchedule;
|
|
6
6
|
readonly limits: {
|
|
7
7
|
readonly dwell?: DwellWindow;
|
|
8
8
|
};
|
|
@@ -11,8 +11,10 @@ export interface EstimatedParticipant {
|
|
|
11
11
|
export interface BundleEstimate {
|
|
12
12
|
/** run.json, which analysis reads whole. */
|
|
13
13
|
readonly runFileBytes: number;
|
|
14
|
-
/** The largest file
|
|
15
|
-
readonly
|
|
14
|
+
/** The largest participant trace file, which analysis also reads whole. */
|
|
15
|
+
readonly largestTraceBytes: number;
|
|
16
|
+
/** observer/index.html, which verify reads whole. */
|
|
17
|
+
readonly observerPageBytes: number;
|
|
16
18
|
/** Every file, screenshots included. */
|
|
17
19
|
readonly totalBytes: number;
|
|
18
20
|
/** Files and folders in the run folder, as the public-safety scan counts them. */
|
|
@@ -33,15 +35,15 @@ export interface BundleEstimate {
|
|
|
33
35
|
}
|
|
34
36
|
/**
|
|
35
37
|
* The bundle a live run of `participants` leaves, each in its session of `sessionMs` unless it
|
|
36
|
-
* declares an end. A participant with an end works its whole window; one without
|
|
37
|
-
* the measured open session above, or its budget
|
|
38
|
-
*
|
|
39
|
-
* session, and the frame after it.
|
|
38
|
+
* declares an end or a cap. A participant with an end works its whole window; one without ends
|
|
39
|
+
* itself, so it is counted for the measured open session above, or its budget (its cap, else
|
|
40
|
+
* `sessionMs`) when that is shorter. Each takes a model turn at the measured pace, and a dwell
|
|
41
|
+
* window adds a frame every `everyMs` for its length, cut to the session, and the frame after it.
|
|
40
42
|
*/
|
|
41
43
|
export declare function estimateBundle(participants: readonly EstimatedParticipant[], sessionMs: number): BundleEstimate;
|
|
42
44
|
/**
|
|
43
45
|
* The warning for a run whose estimate passes a limit, or undefined: each limit it passes, what the
|
|
44
|
-
* estimate counted,
|
|
46
|
+
* estimate counted, and what to change.
|
|
45
47
|
*/
|
|
46
48
|
export declare function bundleLimitReason(estimate: BundleEstimate): string | undefined;
|
|
47
49
|
/** The estimate beside the limits, for `study check`. */
|