humanish 0.117.0 → 0.119.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 +42 -0
- package/dist/analysis/admission.d.ts +23 -4
- package/dist/analysis/admission.js +47 -9
- package/dist/analysis/analysis-limits.d.ts +32 -0
- package/dist/analysis/analysis-limits.js +41 -3
- package/dist/analysis/automatic-config.d.ts +11 -1
- package/dist/analysis/automatic-config.js +26 -11
- package/dist/analysis/cohorts.d.ts +103 -0
- package/dist/analysis/cohorts.js +80 -0
- package/dist/analysis/evidence.d.ts +5 -10
- package/dist/analysis/evidence.js +37 -34
- package/dist/analysis/execute.d.ts +24 -7
- package/dist/analysis/execute.js +290 -307
- package/dist/analysis/responses.d.ts +19 -0
- package/dist/analysis/responses.js +201 -0
- package/dist/analysis/store.js +2 -1
- package/dist/analysis/validation.js +13 -7
- package/dist/cli/commands/analyze.js +10 -4
- package/dist/cli/commands/study-format.js +8 -11
- package/dist/cli/commands/study-route-shared-world.js +3 -1
- package/dist/cli/commands/study-run.js +1 -1
- package/dist/cli/doctor.js +11 -1
- package/dist/comms/lease-store.d.ts +1 -1
- package/dist/comms/lease-store.js +3 -2
- package/dist/comms/receiving-types.d.ts +2 -0
- package/dist/comms/receiving-types.js +2 -0
- package/dist/observer-app.html +2 -2
- package/dist/routes/computer-use/fanout-bundle.js +4 -2
- package/dist/routes/computer-use/fanout-records.js +6 -1
- package/dist/routes/computer-use/participant-execution.d.ts +6 -4
- package/dist/routes/computer-use/participant-execution.js +42 -14
- package/dist/routes/computer-use/participant-model.js +18 -8
- package/dist/routes/computer-use/participant-runs.js +15 -9
- package/dist/routes/computer-use/plan.d.ts +3 -1
- package/dist/routes/computer-use/plan.js +38 -12
- package/dist/routes/computer-use/rerun-selection.js +14 -3
- package/dist/routes/computer-use/single-bundle.js +7 -4
- package/dist/routes/computer-use/subject-projection.js +2 -0
- package/dist/routes/computer-use/types.d.ts +13 -1
- package/dist/routes/scripted/plan.d.ts +4 -1
- package/dist/routes/scripted/plan.js +25 -2
- package/dist/routes/scripted/setup.js +1 -4
- package/dist/routes/scripted/types.d.ts +1 -1
- package/dist/routes/shared-world/bundle-records.js +14 -0
- package/dist/routes/shared-world/bundle.js +2 -1
- package/dist/routes/shared-world/external-public.js +15 -3
- package/dist/routes/shared-world/handoff.d.ts +2 -2
- package/dist/routes/shared-world/participant-specs.js +2 -2
- package/dist/routes/shared-world/plan.d.ts +10 -1
- package/dist/routes/shared-world/plan.js +86 -10
- package/dist/routes/shared-world/provisioned.js +15 -8
- package/dist/routes/shared-world/route.js +7 -4
- package/dist/routes/shared-world/types.d.ts +2 -0
- package/dist/run/bundle-shape.js +12 -2
- package/dist/run/bundle.d.ts +20 -3
- package/dist/run/participant-records.js +1 -0
- package/dist/run/projection.js +7 -2
- package/dist/run/run.js +4 -2
- package/dist/run/subject-name.d.ts +11 -0
- package/dist/run/subject-name.js +11 -0
- package/dist/study/arrivals.d.ts +114 -0
- package/dist/study/arrivals.js +204 -0
- package/dist/study/composition-rules.js +1 -1
- package/dist/study/config.js +6 -20
- package/dist/study/doctor.js +2 -1
- package/dist/study/keys.d.ts +1 -0
- package/dist/study/keys.js +2 -0
- package/dist/study/migrate/v2.js +3 -2
- package/dist/study/parse/actors.d.ts +2 -2
- package/dist/study/parse/actors.js +10 -4
- package/dist/study/parse/front.d.ts +1 -1
- package/dist/study/parse/front.js +36 -2
- package/dist/study/plan-base.d.ts +1 -1
- package/dist/study/plan-participants.d.ts +2 -0
- package/dist/study/plan-participants.js +3 -5
- package/dist/study/plan-types.d.ts +13 -3
- package/dist/study/plan.d.ts +4 -4
- package/dist/study/plan.js +21 -12
- package/dist/study/preflight.d.ts +2 -1
- package/dist/study/preflight.js +24 -3
- package/dist/study/routing.d.ts +5 -4
- package/dist/study/routing.js +16 -7
- package/dist/study/types.d.ts +14 -0
- package/dist/study/validation.d.ts +2 -2
- package/dist/study/validation.js +27 -14
- package/dist/study/warnings.js +11 -3
- package/dist/substrates/e2b/lifetime.d.ts +75 -4
- package/dist/substrates/e2b/lifetime.js +134 -10
- package/dist/substrates/e2b/subject-sandbox.js +2 -6
- package/dist/tui-app.js +112 -112
- package/dist/verify/artifacts.d.ts +19 -4
- package/dist/verify/artifacts.js +39 -8
- package/dist/verify/verify.js +6 -15
- package/docs/contracts/run-bundle.md +8 -7
- package/docs/contracts/schemas.md +33 -9
- package/docs/contracts/study-analysis.md +61 -8
- package/docs/principles/invariants-and-defaults.md +14 -14
- package/docs/product/automatic-analysis.md +10 -3
- package/package.json +1 -1
|
@@ -7,9 +7,13 @@ import { browserSurfaces } from "../../actors/scripted-browser/types.js";
|
|
|
7
7
|
import { resolveAutomaticAnalysis } from "../../analysis/automatic-config.js";
|
|
8
8
|
import { isNonEmpty, planBase, provisionedSubject } from "../../study/plan-base.js";
|
|
9
9
|
import { cloneTargetValidationReason, desktopMediaValidationReason, taskProtocolValidationReason, } from "../../study/validation.js";
|
|
10
|
+
import { sandboxDeadlineRefusal } from "../../substrates/e2b/lifetime.js";
|
|
10
11
|
// Default surface roster is 1 (desktop only): the defaults-table single-participant row governs;
|
|
11
12
|
// `count: 2` is the declared override that adds the mobile surface.
|
|
12
13
|
const DEFAULT_SURFACE_COUNT = 1;
|
|
14
|
+
// Journey wall-clock budget per surface: 5 minutes. A scripted surface has zero model cost and
|
|
15
|
+
// sandbox-seconds are pennies; a short default only truncated slow-loading subjects.
|
|
16
|
+
const DEFAULT_SESSION_TIMEOUT_MS = 300_000;
|
|
13
17
|
/** The app URL a run's evidence names: a provisioned subject's getHost URL is never persisted. */
|
|
14
18
|
export function evidenceAppUrlOf(subject) {
|
|
15
19
|
return subject.kind === "clone" ? "[provisioned-subject]" : subject.appUrl;
|
|
@@ -66,6 +70,26 @@ export function planScriptedStudy(config, input) {
|
|
|
66
70
|
const scenarioRef = config.scenario;
|
|
67
71
|
if (!scenarioRef?.trim())
|
|
68
72
|
return refuse("HUMANISH_SCRIPTED_SCENARIO_INVALID", "A scripted-browser study needs `scenario`: the actor runs the browser steps in that scenario file.", { actor, appUrl: evidenceAppUrlOf(subject) });
|
|
73
|
+
const timeoutMs = config.execution?.timeoutMs;
|
|
74
|
+
const sessionTimeoutMs = timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS;
|
|
75
|
+
// A clone is served from an E2B sandbox that must outlive the journey; a loopback app runs on
|
|
76
|
+
// this machine and needs no sandbox.
|
|
77
|
+
if (subject.kind === "clone") {
|
|
78
|
+
const ceiling = input.sandboxCeiling;
|
|
79
|
+
const reason = ceiling.ok
|
|
80
|
+
? sandboxDeadlineRefusal({
|
|
81
|
+
name: "the subject sandbox",
|
|
82
|
+
sessionMs: sessionTimeoutMs,
|
|
83
|
+
sessionDeclared: timeoutMs !== undefined,
|
|
84
|
+
servedSubject: { seed: subject.state?.seed ?? [] },
|
|
85
|
+
}, ceiling.ms)
|
|
86
|
+
: ceiling.message;
|
|
87
|
+
if (reason)
|
|
88
|
+
return refuse("HUMANISH_SCRIPTED_SUBJECT_INVALID", reason, {
|
|
89
|
+
actor,
|
|
90
|
+
appUrl: evidenceAppUrlOf(subject),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
69
93
|
const requirements = [];
|
|
70
94
|
if (!input.dryRun && subject.kind === "clone") {
|
|
71
95
|
requirements.push({ kind: "key", name: "E2B_API_KEY" });
|
|
@@ -76,7 +100,6 @@ export function planScriptedStudy(config, input) {
|
|
|
76
100
|
if (!input.dryRun && input.injectedBrowser !== true)
|
|
77
101
|
requirements.push({ kind: "host-browser" });
|
|
78
102
|
const persona = config.actor?.persona;
|
|
79
|
-
const timeoutMs = config.execution?.timeoutMs;
|
|
80
103
|
return {
|
|
81
104
|
ok: true,
|
|
82
105
|
plan: {
|
|
@@ -90,7 +113,7 @@ export function planScriptedStudy(config, input) {
|
|
|
90
113
|
scenarioRef,
|
|
91
114
|
surfaces: browserSurfaces.slice(0, config.surfaces?.length ?? DEFAULT_SURFACE_COUNT),
|
|
92
115
|
...(persona === undefined ? {} : { personaId: persona }),
|
|
93
|
-
|
|
116
|
+
sessionTimeoutMs,
|
|
94
117
|
requirements,
|
|
95
118
|
},
|
|
96
119
|
};
|
|
@@ -12,9 +12,6 @@ import { evidenceAppUrlOf } from "./plan.js";
|
|
|
12
12
|
import { resolveScriptedScenario } from "./scenario.js";
|
|
13
13
|
import {} from "./types.js";
|
|
14
14
|
import { refusedResult } from "../../run/study-result.js";
|
|
15
|
-
// Journey wall-clock budget per surface: 5 minutes. A scripted surface has zero model cost and
|
|
16
|
-
// sandbox-seconds are pennies; a short default only truncated slow-loading subjects.
|
|
17
|
-
const DEFAULT_SESSION_TIMEOUT_MS = 300_000;
|
|
18
15
|
/**
|
|
19
16
|
* Checks what the plan cannot: the scenario file, the E2B key and subject env of a live clone,
|
|
20
17
|
* and a browser to launch. Returns the refusal, or what the run needs.
|
|
@@ -83,7 +80,7 @@ export async function prepareScriptedRun(plan, input) {
|
|
|
83
80
|
if (refusal)
|
|
84
81
|
return { ok: false, result: failed(refusal.code, refusal.message) };
|
|
85
82
|
const surfaces = plan.surfaces;
|
|
86
|
-
const timeoutMs = plan.sessionTimeoutMs
|
|
83
|
+
const timeoutMs = plan.sessionTimeoutMs;
|
|
87
84
|
const persona = {
|
|
88
85
|
id: plan.personaId ?? "scripted-journey",
|
|
89
86
|
traitsApplied: [],
|
|
@@ -58,7 +58,7 @@ export interface ScriptedBrowserStudyResult extends AutomaticAnalysisResult, Stu
|
|
|
58
58
|
observer?: ObserverResult;
|
|
59
59
|
warnings: string[];
|
|
60
60
|
error?: {
|
|
61
|
-
code: StudyResultErrorCode | "HUMANISH_SCRIPTED_FAILED" | "HUMANISH_SCRIPTED_ACTOR_UNSUPPORTED" | "HUMANISH_SCRIPTED_SCENARIO_INVALID" | "HUMANISH_SCRIPTED_SUBJECT_UNSAFE" | "HUMANISH_SCRIPTED_BROWSER_MISSING" | "HUMANISH_SCRIPTED_KEYS_MISSING" | "HUMANISH_SCRIPTED_SUBJECT_ENV_MISSING" | "HUMANISH_SCRIPTED_GETHOST_UNAVAILABLE";
|
|
61
|
+
code: StudyResultErrorCode | "HUMANISH_SCRIPTED_FAILED" | "HUMANISH_SCRIPTED_ACTOR_UNSUPPORTED" | "HUMANISH_SCRIPTED_SCENARIO_INVALID" | "HUMANISH_SCRIPTED_SUBJECT_UNSAFE" | "HUMANISH_SCRIPTED_SUBJECT_INVALID" | "HUMANISH_SCRIPTED_BROWSER_MISSING" | "HUMANISH_SCRIPTED_KEYS_MISSING" | "HUMANISH_SCRIPTED_SUBJECT_ENV_MISSING" | "HUMANISH_SCRIPTED_GETHOST_UNAVAILABLE";
|
|
62
62
|
message: string;
|
|
63
63
|
};
|
|
64
64
|
}
|
|
@@ -7,6 +7,7 @@ import { declaredScreenForRender } from "../../substrates/e2b/desktop-geometry.j
|
|
|
7
7
|
import { publicSafeRouteLabel } from "./provenance.js";
|
|
8
8
|
import { participantCaption } from "../../run/participant-caption.js";
|
|
9
9
|
import { participantSummary } from "../../run/outcomes.js";
|
|
10
|
+
import { arrivalRecord } from "../../study/arrivals.js";
|
|
10
11
|
function participantView(ctx, spec, index) {
|
|
11
12
|
const { args, external } = ctx;
|
|
12
13
|
const name = participantCaption({ id: spec.planned.id, personaId: spec.persona.id });
|
|
@@ -69,8 +70,21 @@ function sharedWorldSimulation(ctx, spec, index, view) {
|
|
|
69
70
|
summary: participantSummary(name, { name: "the shared app" }, { trace: session?.trace, sessionError: outcome?.sessionError, inProgress }),
|
|
70
71
|
startedAt: args.run.createdAt,
|
|
71
72
|
updatedAt: args.run.createdAt,
|
|
73
|
+
...(args.plan.arrivals.declared
|
|
74
|
+
? {
|
|
75
|
+
arrival: arrivalRecord(spec.planned.startAfterMs, actorArrival(args.actorResults[index])),
|
|
76
|
+
}
|
|
77
|
+
: {}),
|
|
72
78
|
});
|
|
73
79
|
}
|
|
80
|
+
// A follower the handoff stopped before it opened the app never started its desktop.
|
|
81
|
+
function actorArrival(result) {
|
|
82
|
+
if (result === undefined)
|
|
83
|
+
return undefined;
|
|
84
|
+
return result.outcome.skippedReason === undefined
|
|
85
|
+
? { scheduledAt: result.scheduledAt, startedAt: result.startedAt }
|
|
86
|
+
: { scheduledAt: result.scheduledAt };
|
|
87
|
+
}
|
|
74
88
|
function sharedWorldStream(ctx, spec, index, view) {
|
|
75
89
|
const { args } = ctx;
|
|
76
90
|
const { name, session, screenshots, lastScreenshot, desktopGeometry, screenshotMode } = view;
|
|
@@ -18,6 +18,7 @@ import { sharedWorldParticipantRecords } from "./bundle-records.js";
|
|
|
18
18
|
import { CONCURRENT_ATTRIBUTION_LIMITS, EXTERNAL_PUBLIC_ATTRIBUTION_LIMITS, } from "./types.js";
|
|
19
19
|
import { gapsListClause } from "../../run/outcomes.js";
|
|
20
20
|
import { plural } from "../../run/text.js";
|
|
21
|
+
import { describeArrivals } from "../../study/arrivals.js";
|
|
21
22
|
/** Max windows live at the same instant (sweep over start/end points). The simultaneity
|
|
22
23
|
* count: the participant count says how many existed; this says how many ever ran at once. */
|
|
23
24
|
export function maxSimultaneousWindows(windows) {
|
|
@@ -107,7 +108,7 @@ function planeEvents(args, external) {
|
|
|
107
108
|
at: createdAt,
|
|
108
109
|
level: "info",
|
|
109
110
|
type: "concurrent-shared-world.run.created",
|
|
110
|
-
message: `Created a concurrent shared-world run for ${plan.studyId} (actor ${descriptor.id}, ${plural(actorSpecs.length, "persona")} on one shared app, at most ${plan.concurrency} at once)
|
|
111
|
+
message: `Created a concurrent shared-world run for ${plan.studyId} (actor ${descriptor.id}, ${plural(actorSpecs.length, "persona")} on one shared app, at most ${plan.concurrency} at once).${plan.arrivals.declared ? ` Schedule: ${describeArrivals(plan.arrivals)}` : ""}`,
|
|
111
112
|
});
|
|
112
113
|
// Human-readable plane label, byte-stable for the clone route. local-tree has no repo slug: it
|
|
113
114
|
// labels the packed archive instead (archiveSha256 + dirty/clean when the packed root was a git
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// deadline.
|
|
7
7
|
import { liveObserverResult } from "../../observer/live.js";
|
|
8
8
|
import { redactText, toErrorMessage } from "../../evidence/redaction.js";
|
|
9
|
-
import {
|
|
9
|
+
import { runOnSchedule } from "../../study/arrivals.js";
|
|
10
10
|
import { buildConcurrentSharedWorldBundle, judgeSharedWorldRun } from "./bundle.js";
|
|
11
11
|
import { drainExternalComms } from "./comms.js";
|
|
12
12
|
import { LobbyHandoff, runFollower, runHost } from "./handoff.js";
|
|
@@ -95,10 +95,22 @@ export async function runExternalPublicPlane(ctx, live, inbox) {
|
|
|
95
95
|
.filter(({ index }) => index !== hostIndex);
|
|
96
96
|
const orderedResults = new Array(actorSpecs.length);
|
|
97
97
|
try {
|
|
98
|
+
// The host starts with the run (parse refuses a later start for it); the followers start on
|
|
99
|
+
// the study's schedule.
|
|
98
100
|
const hostPromise = hostIndex >= 0 && actorSpecs[hostIndex] !== undefined
|
|
99
|
-
? runHost(handoff, deps, actorSpecs[hostIndex], hostIndex)
|
|
101
|
+
? runHost(handoff, deps, actorSpecs[hostIndex], hostIndex).then((result) => ({
|
|
102
|
+
...result,
|
|
103
|
+
scheduledAt: result.startedAt,
|
|
104
|
+
}))
|
|
100
105
|
: undefined;
|
|
101
|
-
const followerResultsPromise =
|
|
106
|
+
const followerResultsPromise = runOnSchedule(followerEntries, {
|
|
107
|
+
startAfterMs: ({ spec }) => spec.planned.startAfterMs,
|
|
108
|
+
slots: Math.max(1, concurrency - 1),
|
|
109
|
+
now: ctx.now,
|
|
110
|
+
}, async ({ spec, index }, _position, { scheduledAt }) => ({
|
|
111
|
+
...(await runFollower(handoff, deps, spec, index)),
|
|
112
|
+
scheduledAt,
|
|
113
|
+
}));
|
|
102
114
|
const [hostResult, followerResults] = await Promise.all([hostPromise, followerResultsPromise]);
|
|
103
115
|
if (hostResult !== undefined && hostIndex >= 0) {
|
|
104
116
|
orderedResults[hostIndex] = hostResult;
|
|
@@ -59,5 +59,5 @@ export declare class LobbyHandoff {
|
|
|
59
59
|
lobbyConvergenceDigest: string | undefined;
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
-
export declare function runHost(handoff: LobbyHandoff, deps: HandoffParticipantDeps, spec: DesktopParticipantRun, participantIndex: number): Promise<ActorRunResult
|
|
63
|
-
export declare function runFollower(handoff: LobbyHandoff, deps: HandoffParticipantDeps, spec: DesktopParticipantRun, participantIndex: number): Promise<ActorRunResult
|
|
62
|
+
export declare function runHost(handoff: LobbyHandoff, deps: HandoffParticipantDeps, spec: DesktopParticipantRun, participantIndex: number): Promise<Omit<ActorRunResult, "scheduledAt">>;
|
|
63
|
+
export declare function runFollower(handoff: LobbyHandoff, deps: HandoffParticipantDeps, spec: DesktopParticipantRun, participantIndex: number): Promise<Omit<ActorRunResult, "scheduledAt">>;
|
|
@@ -11,7 +11,7 @@ import { desktopParticipantDeps, } from "../computer-use/participant-deps.js";
|
|
|
11
11
|
import { resolveCommittedPersonasForCwd } from "../../study/persona-resolve.js";
|
|
12
12
|
import { participantAssignment } from "../../study/participant-assignment.js";
|
|
13
13
|
import { redactText } from "../../evidence/redaction.js";
|
|
14
|
-
import {
|
|
14
|
+
import { sandboxHeadroomMs } from "../../substrates/e2b/lifetime.js";
|
|
15
15
|
// prose-check: model prompt (the participant model reads this, not a person)
|
|
16
16
|
const DEFAULT_MISSION = "You are one of MANY users hitting a shared web application at the same time. The browser is already open at the app. Accomplish your role's task, then stop.";
|
|
17
17
|
/** Resolve an actor's participant URL against the harness-minted getHost base (entry is a
|
|
@@ -134,7 +134,7 @@ export function participantRunDeps(ctx, live, scrubKnownValues) {
|
|
|
134
134
|
openaiApiKey: ctx.openaiApiKey,
|
|
135
135
|
e2bApiKey: ctx.e2bApiKey,
|
|
136
136
|
timeoutMs: ctx.timeoutMs,
|
|
137
|
-
sandboxMs: ctx.timeoutMs +
|
|
137
|
+
sandboxMs: ctx.timeoutMs + sandboxHeadroomMs(),
|
|
138
138
|
participantCount: plan.plane.participants.length,
|
|
139
139
|
artifactRoot: ctx.runPaths,
|
|
140
140
|
studyCwd: ctx.cwd,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type CuaActorDescriptor } from "../../actors/registry.js";
|
|
2
2
|
import type { RoutePlanResult, RouteRefusal, SharedWorldPlan } from "../../study/plan-types.js";
|
|
3
3
|
import type { StudyConfig, StudySubjectState } from "../../study/types.js";
|
|
4
|
-
import { type SandboxCeiling } from "../../substrates/e2b/lifetime.js";
|
|
4
|
+
import { type ConcurrentSandboxes, type SandboxCeiling } from "../../substrates/e2b/lifetime.js";
|
|
5
5
|
import type { ConcurrentSharedWorldStudyErrorCode } from "./types.js";
|
|
6
6
|
/** The error a shared-world study returns before a run starts. */
|
|
7
7
|
export interface SharedWorldRefusal extends RouteRefusal<"shared-world", ConcurrentSharedWorldStudyErrorCode> {
|
|
@@ -21,6 +21,15 @@ export declare function planSharedWorldStudy(config: StudyConfig, input: {
|
|
|
21
21
|
readonly hasRunSession?: boolean;
|
|
22
22
|
/** The longest sandbox lifetime the operator's E2B plan allows (sandboxCeiling). */
|
|
23
23
|
readonly sandboxCeiling: SandboxCeiling;
|
|
24
|
+
/** How many sandboxes the operator's E2B plan runs at once (concurrentSandboxes). */
|
|
25
|
+
readonly concurrentSandboxes: ConcurrentSandboxes;
|
|
24
26
|
}): SharedWorldPlanResult;
|
|
27
|
+
/**
|
|
28
|
+
* The schedule lines a shared-world run prints before it creates anything, when the study declares
|
|
29
|
+
* `participants[].startAfterMs`: the schedule, and the worst case in sandbox-minutes, which counts
|
|
30
|
+
* every participant's sandbox (its session plus the teardown buffer) and on the provisioned plane
|
|
31
|
+
* the app's sandbox until the last participant ends.
|
|
32
|
+
*/
|
|
33
|
+
export declare function sharedWorldScheduleLines(plan: SharedWorldPlan): string;
|
|
25
34
|
/** The provisioned plane's declared subject state. The external-public plane declares none. */
|
|
26
35
|
export declare function planeStateOf(plan: SharedWorldPlan): StudySubjectState | undefined;
|
|
@@ -12,8 +12,9 @@ import { PUBLIC_TARGET_OWNER_PATTERN } from "../../study/parse/subject.js";
|
|
|
12
12
|
import { REPO_SLUG_PATTERN } from "../../study/parse/values.js";
|
|
13
13
|
import { concurrentSharedWorldValidationReason, desktopMediaValidationReason, externalPublicSharedWorldValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, taskProtocolValidationReason, waitLimitValidationReason, } from "../../study/validation.js";
|
|
14
14
|
import { unpricedCapCheck } from "../../study/requirements.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { participantsAtOnce, sandboxDeadlineRefusal, sandboxHeadroomMs, } from "../../substrates/e2b/lifetime.js";
|
|
16
|
+
import { boundedConcurrency } from "../computer-use/participant-runs.js";
|
|
17
|
+
import { describeArrivals, planStarts, servedScheduleRefusal, } from "../../study/arrivals.js";
|
|
17
18
|
/** The registered descriptor for a planned actor; planSharedWorldStudy checked the registry. */
|
|
18
19
|
export function sharedWorldDescriptorOf(actorType) {
|
|
19
20
|
const descriptor = actorRegistry[actorType];
|
|
@@ -96,22 +97,32 @@ export function planSharedWorldStudy(config, input) {
|
|
|
96
97
|
const plane = planeOf(config);
|
|
97
98
|
if (plane === undefined)
|
|
98
99
|
throw new Error("shared-world validation admitted a plane it cannot plan");
|
|
100
|
+
const sessionTimeoutMs = config.execution?.timeoutMs ?? defaultSessionTimeoutMs(plane, ceiling.ms);
|
|
101
|
+
const starts = startsOf(plane, config.execution?.concurrency, input.concurrentSandboxes, sessionTimeoutMs);
|
|
102
|
+
if (!starts.ok)
|
|
103
|
+
return refuse(invalid, starts.message, actor);
|
|
104
|
+
const { arrivals, warnings } = starts;
|
|
105
|
+
const deadlineReason = deadlineReasonOf(plane, arrivals, {
|
|
106
|
+
sessionDeclared: config.execution?.timeoutMs !== undefined,
|
|
107
|
+
ceilingMs: ceiling.ms,
|
|
108
|
+
});
|
|
109
|
+
if (deadlineReason)
|
|
110
|
+
return refuse(invalid, deadlineReason, actor);
|
|
99
111
|
const brain = brainOf(config, false);
|
|
100
112
|
if (brain.kind === "caller")
|
|
101
113
|
throw new Error("a shared-world participant has no caller brain");
|
|
102
|
-
const base = planBase(config, {
|
|
103
|
-
dryRun: input.dryRun,
|
|
104
|
-
analysis,
|
|
105
|
-
});
|
|
114
|
+
const base = planBase(config, { dryRun: input.dryRun, analysis });
|
|
106
115
|
return {
|
|
107
116
|
ok: true,
|
|
108
117
|
plan: {
|
|
109
118
|
...base,
|
|
119
|
+
...(warnings.length === 0 ? {} : { warnings }),
|
|
110
120
|
route: "shared-world",
|
|
111
121
|
actor,
|
|
112
122
|
plane,
|
|
113
|
-
concurrency:
|
|
114
|
-
|
|
123
|
+
concurrency: starts.count,
|
|
124
|
+
arrivals,
|
|
125
|
+
sessionTimeoutMs,
|
|
115
126
|
brain,
|
|
116
127
|
caps: planCaps(config),
|
|
117
128
|
requirements: base.dryRun
|
|
@@ -132,6 +143,72 @@ export function planSharedWorldStudy(config, input) {
|
|
|
132
143
|
},
|
|
133
144
|
};
|
|
134
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* How many participants run at once, and when each one starts. Every participant's desktop and, on
|
|
148
|
+
* the provisioned plane, the app's own sandbox share the E2B plan's concurrent sandboxes, and a
|
|
149
|
+
* shared world needs two participants live together. On the external-public plane the host runs
|
|
150
|
+
* on a slot of its own and the others queue for the rest (external-public.ts).
|
|
151
|
+
*/
|
|
152
|
+
function startsOf(plane, declared, limit, sessionMs) {
|
|
153
|
+
const n = plane.participants.length;
|
|
154
|
+
const atOnce = participantsAtOnce({
|
|
155
|
+
concurrency: boundedConcurrency(declared, n),
|
|
156
|
+
declared: declared !== undefined,
|
|
157
|
+
participants: n,
|
|
158
|
+
alongside: plane.kind === "provisioned" ? 1 : 0,
|
|
159
|
+
minimum: 2,
|
|
160
|
+
}, limit);
|
|
161
|
+
if (!atOnce.ok)
|
|
162
|
+
return atOnce;
|
|
163
|
+
const hostIndex = plane.participants.findIndex((participant) => participant.host === true);
|
|
164
|
+
const queue = {
|
|
165
|
+
slots: atOnce.count,
|
|
166
|
+
sessionMs,
|
|
167
|
+
...(plane.kind === "external-public" && hostIndex >= 0 ? { ownSlot: hostIndex } : {}),
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
count: atOnce.count,
|
|
172
|
+
...planStarts(plane.participants, queue, atOnce.lowered),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Why the run's longest-lived sandbox cannot live as long as the plan needs. On the provisioned
|
|
177
|
+
* plane that is the subject sandbox, which serves the app for one session and then until the last
|
|
178
|
+
* participant ends; on the external-public plane, each participant's own sandbox, created at its
|
|
179
|
+
* start.
|
|
180
|
+
*/
|
|
181
|
+
function deadlineReasonOf(plane, arrivals, options) {
|
|
182
|
+
const { sessionDeclared, ceilingMs } = options;
|
|
183
|
+
const sessionMs = arrivals.sessionMs;
|
|
184
|
+
if (plane.kind !== "provisioned")
|
|
185
|
+
return sandboxDeadlineRefusal({ name: "each participant's sandbox", sessionMs, sessionDeclared }, ceilingMs);
|
|
186
|
+
const seed = plane.subject.state.seed ?? [];
|
|
187
|
+
return (sandboxDeadlineRefusal({
|
|
188
|
+
name: "the subject sandbox, which serves the app until every participant ends",
|
|
189
|
+
sessionMs,
|
|
190
|
+
sessionDeclared,
|
|
191
|
+
servedSubject: { seed },
|
|
192
|
+
}, ceilingMs) ?? servedScheduleRefusal(arrivals, { sessionMs, seed }, ceilingMs));
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The schedule lines a shared-world run prints before it creates anything, when the study declares
|
|
196
|
+
* `participants[].startAfterMs`: the schedule, and the worst case in sandbox-minutes, which counts
|
|
197
|
+
* every participant's sandbox (its session plus the teardown buffer) and on the provisioned plane
|
|
198
|
+
* the app's sandbox until the last participant ends.
|
|
199
|
+
*/
|
|
200
|
+
export function sharedWorldScheduleLines(plan) {
|
|
201
|
+
const participants = plan.plane.participants.length;
|
|
202
|
+
const participantMs = participants * (plan.sessionTimeoutMs + sandboxHeadroomMs());
|
|
203
|
+
const subjectMs = plan.plane.kind === "provisioned"
|
|
204
|
+
? plan.arrivals.lastEndMs + sandboxHeadroomMs({ seed: plan.plane.subject.state.seed ?? [] })
|
|
205
|
+
: 0;
|
|
206
|
+
return [
|
|
207
|
+
`humanish shared-world plan (${plan.studyId}): ${participants} participants, at most ${plan.concurrency} at once; worst-case ~${Math.round((participantMs + subjectMs) / 60_000)} sandbox-minutes total${plan.dryRun ? " (dry-run: $0)" : ""}.`,
|
|
208
|
+
` schedule: ${describeArrivals(plan.arrivals)}`,
|
|
209
|
+
"",
|
|
210
|
+
].join("\n");
|
|
211
|
+
}
|
|
135
212
|
// The default per-participant session budget is derived from the plane. On a provisioned plane the
|
|
136
213
|
// binding constraint is the subject sandbox (it must outlive every participant: timeoutMs +
|
|
137
214
|
// provisioning + seeding + teardown buffer, under the sandbox ceiling), so the derivation hands each
|
|
@@ -145,8 +222,7 @@ const DEFAULT_APP_URL_SESSION_MS = 30 * 60_000;
|
|
|
145
222
|
function defaultSessionTimeoutMs(plane, ceilingMs) {
|
|
146
223
|
if (plane.kind !== "provisioned")
|
|
147
224
|
return DEFAULT_APP_URL_SESSION_MS;
|
|
148
|
-
const
|
|
149
|
-
const room = ceilingMs - SUBJECT_PROVISION_BUDGET_MS - stateBudgetMs - SANDBOX_TIMEOUT_BUFFER_MS;
|
|
225
|
+
const room = ceilingMs - sandboxHeadroomMs({ seed: plane.subject.state.seed ?? [] });
|
|
150
226
|
return Math.max(MIN_DERIVED_SESSION_MS, Math.min(MAX_DERIVED_SESSION_MS, room));
|
|
151
227
|
}
|
|
152
228
|
/** The provisioned plane's declared subject state. The external-public plane declares none. */
|
|
@@ -6,7 +6,7 @@ import { buildOriginMap } from "../../comms/capture-surface.js";
|
|
|
6
6
|
import { deployCommsCatch, refreshInboxSurface, writeInboxSurface, } from "../../comms/sandbox-catch.js";
|
|
7
7
|
import { redactText, toErrorMessage } from "../../evidence/redaction.js";
|
|
8
8
|
import { liveObserverResult } from "../../observer/live.js";
|
|
9
|
-
import {
|
|
9
|
+
import { runOnSchedule } from "../../study/arrivals.js";
|
|
10
10
|
import { provisionCloneSubject } from "../../subject/clone.js";
|
|
11
11
|
import { provisionLocalTreeSubject } from "../../subject/local-tree.js";
|
|
12
12
|
import { defaultSharedWorldPhaseSink } from "../../subject/steps.js";
|
|
@@ -96,7 +96,7 @@ class SubjectPlane {
|
|
|
96
96
|
this.setup.stateSnapshots.push({ timestamp, digest: snapshot.digest });
|
|
97
97
|
}
|
|
98
98
|
async acquire() {
|
|
99
|
-
const { plan, deps, env, requestTimeoutMs
|
|
99
|
+
const { plan, deps, env, requestTimeoutMs } = this.ctx;
|
|
100
100
|
const { subjectEnvNames, commsEnv } = this.setup;
|
|
101
101
|
const subjectModule = await (deps.desktopModule ?? loadE2BDesktopModule)();
|
|
102
102
|
// The one subject sandbox: headless service host (no GUI participant). The subject env is
|
|
@@ -107,7 +107,9 @@ class SubjectPlane {
|
|
|
107
107
|
module: subjectModule,
|
|
108
108
|
apiKey: this.ctx.e2bApiKey,
|
|
109
109
|
requestTimeoutMs,
|
|
110
|
-
|
|
110
|
+
// The app serves until the last participant ends, which a late start or a wait for a free
|
|
111
|
+
// slot puts past one session.
|
|
112
|
+
sessionTimeoutMs: plan.arrivals.lastEndMs,
|
|
111
113
|
seed: planeStateOf(plan)?.seed ?? [],
|
|
112
114
|
metadata: {
|
|
113
115
|
...CONCURRENT_SHARED_WORLD_PROVIDER_METADATA,
|
|
@@ -380,15 +382,20 @@ async function publishInProgress(plane, ctx, live, setup) {
|
|
|
380
382
|
}
|
|
381
383
|
startParticipantFlush(ctx, live, inProgressBundle);
|
|
382
384
|
}
|
|
383
|
-
// Launch N actor sandboxes
|
|
384
|
-
// runCuaParticipants would add a pipeline gate and fail-fast). Each actor's window is
|
|
385
|
-
// one orchestrator clock.
|
|
385
|
+
// Launch N actor sandboxes independently on the study's schedule (runCuaParticipant +
|
|
386
|
+
// runOnSchedule; runCuaParticipants would add a pipeline gate and fail-fast). Each actor's window is
|
|
387
|
+
// measured on the one orchestrator clock.
|
|
386
388
|
function runParticipants(plane, ctx, live, setup) {
|
|
387
389
|
const { now } = ctx;
|
|
388
390
|
const participants = ctx.plan.plane.participants;
|
|
389
391
|
const { commsEmail } = setup;
|
|
390
392
|
const baseActorDeps = participantRunDeps(ctx, live, ctx.scrubKnownValues);
|
|
391
|
-
|
|
393
|
+
const schedule = {
|
|
394
|
+
startAfterMs: (spec) => spec.planned.startAfterMs,
|
|
395
|
+
slots: Math.max(1, ctx.concurrency),
|
|
396
|
+
now,
|
|
397
|
+
};
|
|
398
|
+
return runOnSchedule(ctx.actorSpecs, schedule, async (spec, i, { scheduledAt }) => {
|
|
392
399
|
const route = resolveActorEntryUrl(plane.getHostUrl, participants[i]?.entry);
|
|
393
400
|
// Tell this persona its (getHost-reachable) inbox URL, but only when comms is live and this participant
|
|
394
401
|
// has a declared recipient it can actually receive mail into (else it would stall on an empty
|
|
@@ -400,7 +407,7 @@ function runParticipants(plane, ctx, live, setup) {
|
|
|
400
407
|
const startedAt = now();
|
|
401
408
|
const outcome = await runCuaParticipant(actorSpec, { ...baseActorDeps, appUrl: route });
|
|
402
409
|
const endedAt = now();
|
|
403
|
-
return { spec, outcome, startedAt, endedAt, route };
|
|
410
|
+
return { spec, outcome, scheduledAt, startedAt, endedAt, route };
|
|
404
411
|
});
|
|
405
412
|
}
|
|
406
413
|
export async function runProvisionedPlane(ctx, live, setup) {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// per-delta→actor field, so causation under concurrency is structurally inexpressible.
|
|
24
24
|
//
|
|
25
25
|
// Capability and proof: the deterministic $0 gate proves the plumbing + the claims-match-mechanism
|
|
26
|
-
// contract: the real
|
|
26
|
+
// contract: the real start queue (runOnSchedule) produces genuinely overlapping laneWindows (a rendezvous
|
|
27
27
|
// latch in the fake session forces two participant fns in-flight while the real orchestrator clock
|
|
28
28
|
// measures the windows). Every generated bundle describes only its own observations; no one run
|
|
29
29
|
// establishes scale, repeatability, or adopter-harness replacement.
|
|
@@ -38,7 +38,7 @@ import {} from "../../run/run.js";
|
|
|
38
38
|
import { RunSecrets } from "../../run/secrets.js";
|
|
39
39
|
import { admitRoute, completeRefusalAnalysis, } from "../../run/route-shell.js";
|
|
40
40
|
import { runExternalPublicPlane } from "./external-public.js";
|
|
41
|
-
import { sharedWorldDescriptorOf } from "./plan.js";
|
|
41
|
+
import { sharedWorldDescriptorOf, sharedWorldScheduleLines, } from "./plan.js";
|
|
42
42
|
import { runProvisionedPlane } from "./provisioned.js";
|
|
43
43
|
import { concurrentStudyFailure, finishConcurrentRun } from "./result.js";
|
|
44
44
|
import { prepareConcurrentRun } from "./setup.js";
|
|
@@ -62,8 +62,8 @@ export function sharedWorldStudyRefusal(options, refusal) {
|
|
|
62
62
|
studyId: config.id,
|
|
63
63
|
actor: config.actor?.type ?? "",
|
|
64
64
|
participantCount: declared.length,
|
|
65
|
-
//
|
|
66
|
-
//
|
|
65
|
+
// A refused study never planned its concurrency, so its result reports the declared cap, else
|
|
66
|
+
// every declared participant.
|
|
67
67
|
concurrency: config.execution?.concurrency ?? Math.max(1, declared.length),
|
|
68
68
|
dryRun,
|
|
69
69
|
runId: options.runId,
|
|
@@ -143,6 +143,9 @@ async function runPlanInScope(plan, input, config, scope) {
|
|
|
143
143
|
const deps = input.deps ?? {};
|
|
144
144
|
const env = input.env ?? process.env;
|
|
145
145
|
const fail = sharedWorldFailure(plan, input);
|
|
146
|
+
// Before any sandbox or provider call, as computer use prints its participant plan.
|
|
147
|
+
if (plan.arrivals.declared)
|
|
148
|
+
process.stderr.write(sharedWorldScheduleLines(plan));
|
|
146
149
|
const descriptor = sharedWorldDescriptorOf(plan.actor);
|
|
147
150
|
const planeClass = plan.plane.kind === "external-public" ? "external-public" : "provisioned-getHost";
|
|
148
151
|
// The provisioned plane's subject; the external-public plane has none.
|
|
@@ -138,6 +138,8 @@ export interface ConcurrentSharedWorldStudyResult extends AutomaticAnalysisResul
|
|
|
138
138
|
export interface ActorRunResult {
|
|
139
139
|
spec: DesktopParticipantRun;
|
|
140
140
|
outcome: ParticipantRunOutcome;
|
|
141
|
+
/** When the schedule had it start, in epoch ms; the host of an external public app starts with the run. */
|
|
142
|
+
scheduledAt: number;
|
|
141
143
|
startedAt: number;
|
|
142
144
|
endedAt: number;
|
|
143
145
|
route: string;
|
package/dist/run/bundle-shape.js
CHANGED
|
@@ -216,7 +216,8 @@ function isRunSubjectProvenance(value) {
|
|
|
216
216
|
if (value.source !== "clone" &&
|
|
217
217
|
value.source !== "app-url" &&
|
|
218
218
|
value.source !== "local-tree" &&
|
|
219
|
-
value.source !== "desktop-cli"
|
|
219
|
+
value.source !== "desktop-cli" &&
|
|
220
|
+
value.source !== "local-app")
|
|
220
221
|
return false;
|
|
221
222
|
if (value.repo !== undefined && typeof value.repo !== "string")
|
|
222
223
|
return false;
|
|
@@ -312,7 +313,16 @@ function isRunSimulation(value) {
|
|
|
312
313
|
Array.isArray(value.streamIds) &&
|
|
313
314
|
value.streamIds.every((streamId) => typeof streamId === "string") &&
|
|
314
315
|
typeof value.startedAt === "string" &&
|
|
315
|
-
typeof value.updatedAt === "string"
|
|
316
|
+
typeof value.updatedAt === "string" &&
|
|
317
|
+
(value.arrival === undefined || isRunParticipantArrival(value.arrival)));
|
|
318
|
+
}
|
|
319
|
+
function isRunParticipantArrival(value) {
|
|
320
|
+
return (isRecord(value) &&
|
|
321
|
+
typeof value.startAfterMs === "number" &&
|
|
322
|
+
Number.isSafeInteger(value.startAfterMs) &&
|
|
323
|
+
value.startAfterMs >= 0 &&
|
|
324
|
+
(value.scheduledAt === undefined || typeof value.scheduledAt === "string") &&
|
|
325
|
+
(value.startedAt === undefined || typeof value.startedAt === "string"));
|
|
316
326
|
}
|
|
317
327
|
function isRunAdapterArtifact(value) {
|
|
318
328
|
return (isRecord(value) &&
|
package/dist/run/bundle.d.ts
CHANGED
|
@@ -123,6 +123,21 @@ export interface RunSimulation {
|
|
|
123
123
|
streamIds: string[];
|
|
124
124
|
startedAt: string;
|
|
125
125
|
updatedAt: string;
|
|
126
|
+
/**
|
|
127
|
+
* When a computer-use or shared-world participant was due and when it started, in a study that
|
|
128
|
+
* declares `participants[].startAfterMs`. `startAfterMs` is its offset (0 when its entry declares
|
|
129
|
+
* none), `scheduledAt` the moment the run started its participants plus that offset, and
|
|
130
|
+
* `startedAt` when its desktop was requested: later than `scheduledAt` when it waited for a free
|
|
131
|
+
* slot. A dry run records only the offset, and a participant that never started has no
|
|
132
|
+
* `startedAt`. The record's own `startedAt` above is the run's creation time.
|
|
133
|
+
*/
|
|
134
|
+
arrival?: RunParticipantArrival;
|
|
135
|
+
}
|
|
136
|
+
/** A participant record's `arrival`. */
|
|
137
|
+
export interface RunParticipantArrival {
|
|
138
|
+
startAfterMs: number;
|
|
139
|
+
scheduledAt?: string;
|
|
140
|
+
startedAt?: string;
|
|
126
141
|
}
|
|
127
142
|
export interface RunEvent {
|
|
128
143
|
id: string;
|
|
@@ -158,7 +173,8 @@ export interface RunSubjectStateStepRecord {
|
|
|
158
173
|
* pre-existing bundles).
|
|
159
174
|
*/
|
|
160
175
|
export interface RunSubjectProvenance {
|
|
161
|
-
|
|
176
|
+
/** A run saved before `desktop-cli` or `local-app` was recorded says `app-url` for either. */
|
|
177
|
+
source: "clone" | "app-url" | "local-tree" | "desktop-cli" | "local-app";
|
|
162
178
|
/** Clone-route only. Honors policies.redactRepos exactly as the provenance event does. */
|
|
163
179
|
repo?: string;
|
|
164
180
|
/** Desktop-cli only: the product the study declares in `subject.product.name`. */
|
|
@@ -186,8 +202,9 @@ export interface RunSubjectProvenance {
|
|
|
186
202
|
* an external DB is still unpinned overall).
|
|
187
203
|
* declared-not-run: steps declared but not (all) executed ok: dry-run contract bundles
|
|
188
204
|
* and failed live provisioning.
|
|
189
|
-
* undeclared: no subject.state block (stateless apps, app-url and desktop-cli
|
|
190
|
-
* the explicit "absence declared" marker that provenance requires when there
|
|
205
|
+
* undeclared: no subject.state block (stateless apps, app-url, local-app and desktop-cli
|
|
206
|
+
* subjects), the explicit "absence declared" marker that provenance requires when there
|
|
207
|
+
* is none.
|
|
191
208
|
* external-public: an operator-declared, operator-owned public deployment used
|
|
192
209
|
* directly as the shared plane; humanish neither provisioned nor seeded it (no getHost, no
|
|
193
210
|
* clone, no in-sandbox filesystem). It is not "seeded" (nothing was seeded) and not "unpinned" (this is
|
package/dist/run/projection.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// - an unknown cost is `null` (declared absent), never 0;
|
|
8
8
|
// - truncation is reported, never silent;
|
|
9
9
|
// - a participant's reported thinking is quoted, never paraphrased or summarized.
|
|
10
|
+
import { indexedRunCost } from "./run-cost.js";
|
|
10
11
|
/**
|
|
11
12
|
* Group runs by study, newest-first within each study and by recency between studies. Runs with no study
|
|
12
13
|
* attribution are collected under `unattributed` rather than invented into a study. That is the
|
|
@@ -74,9 +75,13 @@ export function expectationFor(entries, mode) {
|
|
|
74
75
|
.map((entry) => entry.durationMs)
|
|
75
76
|
.filter((value) => typeof value === "number" && Number.isFinite(value) && value >= 0)
|
|
76
77
|
.sort((a, b) => a - b);
|
|
78
|
+
// Each run's cost is the one its row and card show (src/run/run-cost.ts): participants and
|
|
79
|
+
// desktops plus its analyses. A run whose participants and desktops have no figure stays
|
|
80
|
+
// unpriced below, so an analysis alone never stands in for what a run cost.
|
|
77
81
|
const costs = finished
|
|
78
|
-
.
|
|
79
|
-
.
|
|
82
|
+
.filter((entry) => typeof entry.estimatedCostUsd === "number" && Number.isFinite(entry.estimatedCostUsd))
|
|
83
|
+
.map((entry) => indexedRunCost(entry).total?.usd)
|
|
84
|
+
.filter((value) => value !== undefined)
|
|
80
85
|
.sort((a, b) => a - b);
|
|
81
86
|
// A cost is unknown whether it was declared absent (`null`) or never recorded at all
|
|
82
87
|
// (`undefined`: every fail-closed exit finalized with no outcome). Counting only the first
|
package/dist/run/run.js
CHANGED
|
@@ -117,13 +117,15 @@ function observerTarget(options) {
|
|
|
117
117
|
function runPublisher(args) {
|
|
118
118
|
const { options, runId, createdAt, paths, runStatus, now, admit, participantsRan } = args;
|
|
119
119
|
const observer = observerTarget(options);
|
|
120
|
-
// The study's warnings join whatever events the route wrote, once, in every write.
|
|
120
|
+
// The study's warnings join whatever events the route wrote, once, in every write. A warning can
|
|
121
|
+
// quote the study file, which may name a sandbox URL a participant needs, so each goes through
|
|
122
|
+
// the redaction a run failure gets.
|
|
121
123
|
const studyWarnings = (options.warnings ?? []).map((message, index) => ({
|
|
122
124
|
id: `event-study-warning-${String(index + 1).padStart(3, "0")}`,
|
|
123
125
|
at: createdAt,
|
|
124
126
|
level: "warn",
|
|
125
127
|
type: "study.warning",
|
|
126
|
-
message,
|
|
128
|
+
message: redactText(message),
|
|
127
129
|
}));
|
|
128
130
|
const withStudyWarnings = (events) => [
|
|
129
131
|
...events.filter((event) => !studyWarnings.some((warning) => warning.id === event.id)),
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** A subject as a run records it: its source and, for a product used at a desktop, the product. */
|
|
2
|
+
interface NamedSubject {
|
|
3
|
+
readonly source: string;
|
|
4
|
+
readonly product?: string | undefined;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The URL a participant opens names the subject. A product used at a desktop has no URL, so it is
|
|
8
|
+
* named with its source, as in `humanish (desktop-cli)`. Empty when there is neither.
|
|
9
|
+
*/
|
|
10
|
+
export declare function subjectName(url: string, subject: NamedSubject): string;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The words a reader sees for a run's subject. The `subject:` line of `humanish run`, the run's
|
|
2
|
+
// summary and each participant's sentence name it the same way.
|
|
3
|
+
/**
|
|
4
|
+
* The URL a participant opens names the subject. A product used at a desktop has no URL, so it is
|
|
5
|
+
* named with its source, as in `humanish (desktop-cli)`. Empty when there is neither.
|
|
6
|
+
*/
|
|
7
|
+
export function subjectName(url, subject) {
|
|
8
|
+
if (url !== "" || subject.product === undefined)
|
|
9
|
+
return url;
|
|
10
|
+
return `${subject.product} (${subject.source})`;
|
|
11
|
+
}
|