humanish 0.125.0 → 0.126.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 +16 -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 +8 -5
- package/dist/actors/computer-use/loop/ending.js +8 -5
- 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/contract.d.ts +31 -8
- package/dist/actors/contract.js +3 -0
- package/dist/actors/stop-cause.js +1 -0
- 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/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-records.js +4 -0
- 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-model.js +16 -6
- package/dist/routes/computer-use/participant-runs.d.ts +2 -2
- package/dist/routes/computer-use/participant-runs.js +11 -7
- package/dist/routes/computer-use/plan.js +39 -1
- package/dist/routes/computer-use/setup.d.ts +1 -0
- package/dist/routes/computer-use/setup.js +28 -0
- package/dist/routes/computer-use/single-bundle.d.ts +5 -1
- package/dist/routes/computer-use/single-bundle.js +2 -0
- package/dist/routes/computer-use/types.d.ts +16 -10
- package/dist/routes/shared-world/bundle-records.js +3 -0
- package/dist/routes/shared-world/participant-specs.js +3 -2
- package/dist/routes/shared-world/plan.js +5 -0
- package/dist/run/contained-output.d.ts +7 -3
- package/dist/run/contained-output.js +11 -7
- package/dist/run/judge.d.ts +7 -4
- package/dist/run/judge.js +13 -8
- package/dist/run/live-items.d.ts +19 -0
- package/dist/run/live-items.js +28 -8
- 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 +32 -14
- package/dist/study/arrivals.js +47 -21
- package/dist/study/bundle-estimate.d.ts +6 -5
- package/dist/study/bundle-estimate.js +12 -7
- 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 -5
- package/dist/study/plan-participants.js +17 -1
- package/dist/study/plan.js +24 -13
- package/dist/study/preflight.js +4 -0
- package/dist/study/types.d.ts +34 -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/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 +72 -3
- package/package.json +2 -2
|
@@ -13,7 +13,7 @@ import { type CuaDiagnostics } from "./diagnostics.js";
|
|
|
13
13
|
import type { ActorCompletionReason, ActorStatus, ActorStopCause, ActorTokenUsage, ActorTraceItem } from "../../actors/contract.js";
|
|
14
14
|
import { type CuaActorDescriptor } from "../../actors/registry.js";
|
|
15
15
|
import type { StudyDeps } from "../../study/study-deps.js";
|
|
16
|
-
import type { ArrivalPlan, ParticipantArrival } from "../../study/arrivals.js";
|
|
16
|
+
import type { ArrivalPlan, ParticipantArrival, ParticipantEnd } from "../../study/arrivals.js";
|
|
17
17
|
import type { StudyEvent, ParticipantRef } from "../../study/run-study-events.js";
|
|
18
18
|
import type { InProcessDriver, ProviderFactory, RunStudyHomes } from "../../study/run-study-homes.js";
|
|
19
19
|
import { type BrowserScorer } from "../../study/adapter-extension.js";
|
|
@@ -23,9 +23,10 @@ import { type DesktopResourceObservation } from "../../substrates/e2b/desktop-re
|
|
|
23
23
|
import { type DetachedTimers } from "../../substrates/detached.js";
|
|
24
24
|
import type { Brain, ComputerUsePlan, ComputerUseRunner, ResidualConfig } from "../../study/plan-types.js";
|
|
25
25
|
import { type StudyCommsEmail } from "../../study/types.js";
|
|
26
|
+
import type { DesktopFile } from "../../study/desktop-files.js";
|
|
26
27
|
import { type ObserverResult } from "../../observer/render.js";
|
|
27
28
|
import { type BundleRun, type RunBundle, type RunRerunLineage, type RunScorerProvenance, type RunSubjectProvenance, type RunSubjectStateStepRecord } from "../../run/bundle.js";
|
|
28
|
-
import { type RunDesktopGeometry } from "../../run/streams.js";
|
|
29
|
+
import { type RunDesktopFile, type RunDesktopGeometry } from "../../run/streams.js";
|
|
29
30
|
import { type PreparedOutputRoot } from "../../run/contained-output.js";
|
|
30
31
|
import type { ComputerUseParticipant, SharedWorldParticipant } from "../../study/plan-participants.js";
|
|
31
32
|
import type { ResolvedParticipant } from "../../run/participant.js";
|
|
@@ -114,6 +115,8 @@ export interface CuaParticipantPlanEntry {
|
|
|
114
115
|
startAfterMs?: number;
|
|
115
116
|
/** The participant's declared end, on the same clock. */
|
|
116
117
|
endAfterMs?: number;
|
|
118
|
+
/** The participant's session cap, from the start of its session. */
|
|
119
|
+
maxSessionMs?: number;
|
|
117
120
|
}
|
|
118
121
|
/** The pre-flight spend and participant plan (pure; printed to stderr + recorded as a bundle event before
|
|
119
122
|
* any sandbox or provider call; identical in dry-run, marked $0). */
|
|
@@ -195,7 +198,7 @@ export interface CuaParticipantSummary {
|
|
|
195
198
|
concurrency: number;
|
|
196
199
|
waves: number;
|
|
197
200
|
}
|
|
198
|
-
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";
|
|
201
|
+
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_DESKTOP_FILES_INVALID" | "HUMANISH_COMPUTER_USE_TIME_ZONE_UNSUPPORTED" | "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";
|
|
199
202
|
export interface CuaActorStudyResult extends AutomaticAnalysisResult, StudyResultIdentity<"computer-use"> {
|
|
200
203
|
/** True when the Observer verified the bundle, all live participants passed credibility checks
|
|
201
204
|
* (or this is a dry-run), and no declared adapter/scorer verdict failed. */
|
|
@@ -316,6 +319,9 @@ export interface E2BDesktopDeps {
|
|
|
316
319
|
/** Local-tree route only: the once-per-run packed archive bytes, shared byte-identically
|
|
317
320
|
* across every fan-out participant's upload step. Absent on dry-run and every other route. */
|
|
318
321
|
localTreeArchiveBuffer?: ArrayBuffer;
|
|
322
|
+
/** Computer use on a hosted desktop: each participant's files (participants[].files) by id,
|
|
323
|
+
* read on this machine before any sandbox. A participant without an entry receives none. */
|
|
324
|
+
desktopFiles?: ReadonlyMap<string, readonly DesktopFile[]>;
|
|
319
325
|
env: Record<string, string | undefined>;
|
|
320
326
|
e2bApiKey: string;
|
|
321
327
|
requestTimeoutMs: number;
|
|
@@ -367,14 +373,12 @@ export interface ParticipantModelDeps {
|
|
|
367
373
|
openaiApiKey: string;
|
|
368
374
|
timeoutMs: number;
|
|
369
375
|
/**
|
|
370
|
-
*
|
|
371
|
-
* (scheduledParticipantDeps)
|
|
376
|
+
* How the participant's session ends, when it declares participants[].endAfterMs or maxSessionMs
|
|
377
|
+
* (scheduledParticipantDeps): at its scheduled end on `now`'s clock, with stop cause
|
|
378
|
+
* `scheduled_end`, or at its cap after the session starts, with stop cause `session_cap`.
|
|
372
379
|
*/
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
readonly windDownMs: number;
|
|
376
|
-
};
|
|
377
|
-
/** The run's clock. A participant with a scheduled end runs its session on it. */
|
|
380
|
+
end?: ParticipantEnd;
|
|
381
|
+
/** The run's clock. A participant with an end or a cap runs its session on it. */
|
|
378
382
|
now: () => number;
|
|
379
383
|
participantCount: number;
|
|
380
384
|
artifactRoot: PreparedOutputRoot;
|
|
@@ -449,6 +453,8 @@ export interface ParticipantRunOutcome {
|
|
|
449
453
|
/** Requested + measured desktop/browser geometry. Viewport is absent when measurement failed. */
|
|
450
454
|
desktopGeometry?: RunDesktopGeometry;
|
|
451
455
|
recording?: RunDesktopRecording;
|
|
456
|
+
/** participants[].files confirmed on the participant's desktop. */
|
|
457
|
+
desktopFiles?: RunDesktopFile[];
|
|
452
458
|
stateStepRecords: RunSubjectStateStepRecord[];
|
|
453
459
|
/** Completed subject-phase records (clone/upload/extract/install/build/ready/state groups),
|
|
454
460
|
* folded into bundle.events at build time. Empty on the in-process route (no provisioning). */
|
|
@@ -122,6 +122,9 @@ function sharedWorldStream(ctx, spec, index, view) {
|
|
|
122
122
|
},
|
|
123
123
|
}),
|
|
124
124
|
desktopGeometry,
|
|
125
|
+
...(spec.planned.limits.timeZone === undefined
|
|
126
|
+
? {}
|
|
127
|
+
: { timeZone: spec.planned.limits.timeZone }),
|
|
125
128
|
ui: {
|
|
126
129
|
route: view.route,
|
|
127
130
|
intent: `Watch ${name} use the shared app at the same time as the others.`,
|
|
@@ -140,10 +140,11 @@ export function participantRunDeps(ctx, live, scrubKnownValues) {
|
|
|
140
140
|
studyCwd: ctx.cwd,
|
|
141
141
|
redactScreenshots: ctx.redactScreenshots,
|
|
142
142
|
scrubKnownValues,
|
|
143
|
-
// A session that throws returns no trace; the run still records that it started.
|
|
143
|
+
// A session that throws returns no trace; the run still records that it started. Observed
|
|
144
|
+
// URLs land in a shared-world bundle only as digests, so no session records its pages.
|
|
144
145
|
runSession: (options) => {
|
|
145
146
|
ctx.run.participantStarted();
|
|
146
|
-
return ctx.runSession(options);
|
|
147
|
+
return ctx.runSession({ ...options, recordPages: false });
|
|
147
148
|
},
|
|
148
149
|
liveFlush: () => live.flush?.flush,
|
|
149
150
|
onStream: participantStreams(ctx.input.onStream, live),
|
|
@@ -8,6 +8,8 @@ import { actorRegistry, isCuaActorDescriptor, } from "../../actors/registry.js";
|
|
|
8
8
|
import { resolveAutomaticAnalysis } from "../../analysis/automatic-config.js";
|
|
9
9
|
import { brainOf, planCaps, desktopRequirements, isNonEmpty, planBase, } from "../../study/plan-base.js";
|
|
10
10
|
import { sharedWorldParticipants } from "../../study/plan-participants.js";
|
|
11
|
+
import { DESKTOP_FILES_ROUTE_REASON } from "../../study/desktop-files.js";
|
|
12
|
+
import { participantList } from "../../study/study-fields.js";
|
|
11
13
|
import { PUBLIC_TARGET_OWNER_PATTERN } from "../../study/parse/subject.js";
|
|
12
14
|
import { REPO_SLUG_PATTERN } from "../../study/parse/values.js";
|
|
13
15
|
import { concurrentSharedWorldValidationReason, desktopMediaValidationReason, externalPublicSharedWorldValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, taskProtocolValidationReason, waitLimitValidationReason, } from "../../study/validation.js";
|
|
@@ -41,6 +43,9 @@ export function planSharedWorldStudy(config, input) {
|
|
|
41
43
|
const tasksReason = taskProtocolValidationReason(config, false);
|
|
42
44
|
if (tasksReason)
|
|
43
45
|
return refuse("HUMANISH_STUDY_TASKS_UNSUPPORTED", tasksReason);
|
|
46
|
+
// The parser refuses these; a library caller's config skips it.
|
|
47
|
+
if (participantList(config)?.some((entry) => entry.files !== undefined))
|
|
48
|
+
return refuse(invalid, `participants[].files: ${DESKTOP_FILES_ROUTE_REASON}. Remove them.`);
|
|
44
49
|
const actorType = config.actor?.type ?? "";
|
|
45
50
|
const descriptor = actorRegistry[actorType];
|
|
46
51
|
if (!descriptor || !isCuaActorDescriptor(descriptor))
|
|
@@ -117,10 +117,14 @@ export declare function readOpenedAtMost(handle: FileHandle, maxBytes: number):
|
|
|
117
117
|
/**
|
|
118
118
|
* The first `length` bytes of one regular file, under the checks readContainedRegularFile makes,
|
|
119
119
|
* or all of a file that holds fewer. Bytes past `length`, such as a later append, are not read.
|
|
120
|
+
* With `start`, the bytes from `start` to `length`.
|
|
120
121
|
*/
|
|
121
|
-
export declare function readContainedPrefix(rootInput: PreparedOutputRoot, relativePath: string, length: number): Promise<ContainedRead>;
|
|
122
|
-
/**
|
|
123
|
-
|
|
122
|
+
export declare function readContainedPrefix(rootInput: PreparedOutputRoot, relativePath: string, length: number, start?: number): Promise<ContainedRead>;
|
|
123
|
+
/**
|
|
124
|
+
* The first `length` bytes of an opened file, read by position, or all of a shorter one. With
|
|
125
|
+
* `start`, the bytes from `start` to `length`.
|
|
126
|
+
*/
|
|
127
|
+
export declare function readOpenedPrefix(handle: FileHandle, length: number, start?: number): Promise<Buffer>;
|
|
124
128
|
/** The caller owns this checked descriptor and must close it after reading/streaming. */
|
|
125
129
|
export declare function openContainedRegularFile(rootInput: PreparedOutputRoot, relativePath: string): Promise<FileHandle | null>;
|
|
126
130
|
export declare function assertSafeOutputPathSegment(value: string, label?: string): void;
|
|
@@ -307,15 +307,16 @@ export async function readOpenedAtMost(handle, maxBytes) {
|
|
|
307
307
|
/**
|
|
308
308
|
* The first `length` bytes of one regular file, under the checks readContainedRegularFile makes,
|
|
309
309
|
* or all of a file that holds fewer. Bytes past `length`, such as a later append, are not read.
|
|
310
|
+
* With `start`, the bytes from `start` to `length`.
|
|
310
311
|
*/
|
|
311
|
-
export async function readContainedPrefix(rootInput, relativePath, length) {
|
|
312
|
+
export async function readContainedPrefix(rootInput, relativePath, length, start = 0) {
|
|
312
313
|
const opened = await openContained(rootInput, relativePath);
|
|
313
314
|
if (opened.status === "missing")
|
|
314
315
|
return opened;
|
|
315
316
|
if (opened.status === "refused")
|
|
316
317
|
return { status: "refused", reason: opened.reason, limit: length };
|
|
317
318
|
try {
|
|
318
|
-
return { status: "read", bytes: await readOpenedPrefix(opened.handle, length) };
|
|
319
|
+
return { status: "read", bytes: await readOpenedPrefix(opened.handle, length, start) };
|
|
319
320
|
}
|
|
320
321
|
catch {
|
|
321
322
|
return { status: "refused", reason: "unreadable", limit: length };
|
|
@@ -324,12 +325,15 @@ export async function readContainedPrefix(rootInput, relativePath, length) {
|
|
|
324
325
|
await opened.handle.close().catch(() => { });
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
|
-
/**
|
|
328
|
-
|
|
329
|
-
|
|
328
|
+
/**
|
|
329
|
+
* The first `length` bytes of an opened file, read by position, or all of a shorter one. With
|
|
330
|
+
* `start`, the bytes from `start` to `length`.
|
|
331
|
+
*/
|
|
332
|
+
export async function readOpenedPrefix(handle, length, start = 0) {
|
|
333
|
+
const bytes = Buffer.allocUnsafe(Math.max(0, length - start));
|
|
330
334
|
let total = 0;
|
|
331
|
-
while (total < length) {
|
|
332
|
-
const { bytesRead } = await handle.read(bytes, total, length - total, total);
|
|
335
|
+
while (total < bytes.length) {
|
|
336
|
+
const { bytesRead } = await handle.read(bytes, total, bytes.length - total, start + total);
|
|
333
337
|
if (bytesRead === 0)
|
|
334
338
|
break;
|
|
335
339
|
total += bytesRead;
|
package/dist/run/judge.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface BlockerFacts {
|
|
|
6
6
|
completionReason: ActorCompletionReason;
|
|
7
7
|
/** A harness-owned stop condition ended the session: a matched stopWhen or a completed dwell window. */
|
|
8
8
|
stopConditionMatched: boolean;
|
|
9
|
-
/** What the participant declared
|
|
9
|
+
/** What the participant declared, when it declared an outcome (ActorTrace.declaredOutcome). */
|
|
10
10
|
declaredOutcome?: ParticipantDeclaredOutcome;
|
|
11
11
|
/** The route's reading of the closing report: it describes a blocker or asks for instructions. */
|
|
12
12
|
closingReportReadsBlocked: boolean;
|
|
@@ -22,9 +22,12 @@ export interface SessionEnding extends BlockerFacts {
|
|
|
22
22
|
*/
|
|
23
23
|
export declare function hollowCompletion(ending: SessionEnding): boolean;
|
|
24
24
|
/**
|
|
25
|
-
* The
|
|
26
|
-
* word and wins.
|
|
27
|
-
* session: a matched stopWhen is structured completion evidence and overrides
|
|
25
|
+
* The session ended goal_satisfied but the participant reported a blocker. A declared `reached`
|
|
26
|
+
* or `blocked` is its own word and wins. Otherwise the closing report is read, unless a stop
|
|
27
|
+
* condition ended the session: a matched stopWhen is structured completion evidence and overrides
|
|
28
|
+
* the text. A declared `not_reached` is read the same way: it ends the session as gave_up when
|
|
29
|
+
* the participant ends it, so here it comes from a closing report at a scheduled end or a session
|
|
30
|
+
* cap, and it does not say whether something stopped the participant.
|
|
28
31
|
*/
|
|
29
32
|
export declare function selfReportedBlocker(ending: BlockerFacts): boolean;
|
|
30
33
|
/** What a participant's pass reads: its session's end, or why it has none, and the two judgments above. */
|
package/dist/run/judge.js
CHANGED
|
@@ -25,16 +25,21 @@ export function hollowCompletion(ending) {
|
|
|
25
25
|
!ending.stopConditionMatched);
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
* The
|
|
29
|
-
* word and wins.
|
|
30
|
-
* session: a matched stopWhen is structured completion evidence and overrides
|
|
28
|
+
* The session ended goal_satisfied but the participant reported a blocker. A declared `reached`
|
|
29
|
+
* or `blocked` is its own word and wins. Otherwise the closing report is read, unless a stop
|
|
30
|
+
* condition ended the session: a matched stopWhen is structured completion evidence and overrides
|
|
31
|
+
* the text. A declared `not_reached` is read the same way: it ends the session as gave_up when
|
|
32
|
+
* the participant ends it, so here it comes from a closing report at a scheduled end or a session
|
|
33
|
+
* cap, and it does not say whether something stopped the participant.
|
|
31
34
|
*/
|
|
32
35
|
export function selfReportedBlocker(ending) {
|
|
33
|
-
if (ending.
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
if (ending.completionReason !== "goal_satisfied")
|
|
37
|
+
return false;
|
|
38
|
+
if (ending.declaredOutcome === "blocked")
|
|
39
|
+
return true;
|
|
40
|
+
if (ending.declaredOutcome === "reached")
|
|
41
|
+
return false;
|
|
42
|
+
return ending.closingReportReadsBlocked && !ending.stopConditionMatched;
|
|
38
43
|
}
|
|
39
44
|
/** Judge participant evidence for the bundle and the result in the same order. */
|
|
40
45
|
export function judgeParticipantRecords(participants, wording = {}) {
|
package/dist/run/live-items.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ export declare const LIVE_ACTOR_SCHEMA = "humanish.live-actor.v2";
|
|
|
8
8
|
export declare const LIVE_ACTOR_V1_SCHEMA = "humanish.live-actor.v1";
|
|
9
9
|
/** Whether `value` is a live items index naming a live file name and its own generation. */
|
|
10
10
|
export declare function isLiveItemsIndex(value: unknown): value is LiveItemsIndex;
|
|
11
|
+
/** Where a reader's copy of a live file ends: its first `items` lines, `bytes` bytes. */
|
|
12
|
+
export interface LiveItemsCursor {
|
|
13
|
+
items: number;
|
|
14
|
+
bytes: number;
|
|
15
|
+
}
|
|
11
16
|
/**
|
|
12
17
|
* The items the indexed prefix of a live file holds, or why the bytes do not match the index:
|
|
13
18
|
* `bytes` must be exactly the indexed byte count, hash to the indexed sha256, and hold the indexed
|
|
@@ -18,6 +23,20 @@ export declare function liveItemsFromBytes(bytes: Buffer, index: LiveItemsIndex)
|
|
|
18
23
|
} | {
|
|
19
24
|
refused: string;
|
|
20
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* The items past `after` in the indexed prefix of a live file, from the file's bytes between
|
|
28
|
+
* `after.bytes - 1` and the indexed end, or why they do not fit: the first byte must end the line
|
|
29
|
+
* before the cursor, and the rest must hold exactly the lines between the cursor's count and the
|
|
30
|
+
* indexed count, each a JSON object. A line end at `after.bytes - 1` fixes how many lines precede
|
|
31
|
+
* it, so a cursor whose `items` is not that count fails on the line count. The sha256 run.json
|
|
32
|
+
* records covers the whole prefix and is not checked here: what precedes the cursor is the
|
|
33
|
+
* reader's, and a generation's bytes never change once written.
|
|
34
|
+
*/
|
|
35
|
+
export declare function liveItemsAfter(bytes: Buffer, index: LiveItemsIndex, after: LiveItemsCursor): {
|
|
36
|
+
items: ActorTraceItem[];
|
|
37
|
+
} | {
|
|
38
|
+
refused: string;
|
|
39
|
+
};
|
|
21
40
|
/** One run's live files: what each stream position's current generation holds. */
|
|
22
41
|
export interface LiveItemsWriter {
|
|
23
42
|
/**
|
package/dist/run/live-items.js
CHANGED
|
@@ -41,20 +41,40 @@ export function liveItemsFromBytes(bytes, index) {
|
|
|
41
41
|
};
|
|
42
42
|
if (createHash("sha256").update(bytes).digest("hex") !== index.sha256)
|
|
43
43
|
return { refused: `${index.path} does not match the sha256 run.json records for it` };
|
|
44
|
+
return itemLinesOf(bytes, index.items, index.path);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The items past `after` in the indexed prefix of a live file, from the file's bytes between
|
|
48
|
+
* `after.bytes - 1` and the indexed end, or why they do not fit: the first byte must end the line
|
|
49
|
+
* before the cursor, and the rest must hold exactly the lines between the cursor's count and the
|
|
50
|
+
* indexed count, each a JSON object. A line end at `after.bytes - 1` fixes how many lines precede
|
|
51
|
+
* it, so a cursor whose `items` is not that count fails on the line count. The sha256 run.json
|
|
52
|
+
* records covers the whole prefix and is not checked here: what precedes the cursor is the
|
|
53
|
+
* reader's, and a generation's bytes never change once written.
|
|
54
|
+
*/
|
|
55
|
+
export function liveItemsAfter(bytes, index, after) {
|
|
56
|
+
if (bytes.length !== index.bytes - after.bytes + 1)
|
|
57
|
+
return {
|
|
58
|
+
refused: `${index.path} holds ${bytes.length - 1} bytes past byte ${after.bytes} and run.json records ${index.bytes - after.bytes}`,
|
|
59
|
+
};
|
|
60
|
+
if (bytes[0] !== 0x0a)
|
|
61
|
+
return { refused: `${index.path} has no line end before byte ${after.bytes}` };
|
|
62
|
+
return itemLinesOf(bytes.subarray(1), index.items - after.items, index.path);
|
|
63
|
+
}
|
|
64
|
+
/** The JSON object on each of `count` lines in `bytes`, which must be UTF-8 and end a line. */
|
|
65
|
+
function itemLinesOf(bytes, count, file) {
|
|
44
66
|
let text;
|
|
45
67
|
try {
|
|
46
68
|
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
47
69
|
}
|
|
48
70
|
catch {
|
|
49
|
-
return { refused: `${
|
|
71
|
+
return { refused: `${file} is not UTF-8 text` };
|
|
50
72
|
}
|
|
51
73
|
const lines = text === "" ? [] : text.split("\n");
|
|
52
74
|
if (lines.length > 0 && lines.pop() !== "")
|
|
53
|
-
return { refused: `${
|
|
54
|
-
if (lines.length !==
|
|
55
|
-
return {
|
|
56
|
-
refused: `${index.path} holds ${lines.length} items and run.json records ${index.items}`,
|
|
57
|
-
};
|
|
75
|
+
return { refused: `${file} does not end its indexed bytes at a line end` };
|
|
76
|
+
if (lines.length !== count)
|
|
77
|
+
return { refused: `${file} holds ${lines.length} items and run.json records ${count}` };
|
|
58
78
|
const items = [];
|
|
59
79
|
for (const line of lines) {
|
|
60
80
|
let item;
|
|
@@ -62,10 +82,10 @@ export function liveItemsFromBytes(bytes, index) {
|
|
|
62
82
|
item = JSON.parse(line);
|
|
63
83
|
}
|
|
64
84
|
catch {
|
|
65
|
-
return { refused: `${
|
|
85
|
+
return { refused: `${file} holds a line that is not JSON` };
|
|
66
86
|
}
|
|
67
87
|
if (!isRecord(item))
|
|
68
|
-
return { refused: `${
|
|
88
|
+
return { refused: `${file} holds a line that is not an item` };
|
|
69
89
|
items.push(item);
|
|
70
90
|
}
|
|
71
91
|
return { items };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type RunBundle } from "./bundle.js";
|
|
2
2
|
import { type PreparedOutputRoot } from "./contained-output.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type LiveItemsCursor } from "./live-items.js";
|
|
4
|
+
import type { LiveItemsIndex, RunStream } from "./streams.js";
|
|
4
5
|
/** A trace that does not match its index, or cannot be read. The message names the stream. */
|
|
5
6
|
export declare class StreamTraceRefusedError extends Error {
|
|
6
7
|
readonly streamId: string;
|
|
@@ -23,15 +24,27 @@ export declare function splitStreamTraces(bundle: RunBundle): {
|
|
|
23
24
|
/**
|
|
24
25
|
* Where a loader reads trace files from: the bytes at a run-relative path, at most `maxBytes`, or
|
|
25
26
|
* why it could not. With `length`, the first `length` bytes, or all of a shorter file: a live file
|
|
26
|
-
* grows past what run.json indexes, and the loader reads only the indexed prefix.
|
|
27
|
-
*
|
|
27
|
+
* grows past what run.json indexes, and the loader reads only the indexed prefix. With `start` as
|
|
28
|
+
* well, the bytes from `start` to `length`: the items past a reader's cursor. Prepared run paths
|
|
29
|
+
* read through the contained reader; the Observer server passes a reader over its pinned run
|
|
28
30
|
* folder.
|
|
29
31
|
*/
|
|
30
|
-
export type TraceFileReader = (relativePath: string, maxBytes: number, length?: number) => Promise<{
|
|
32
|
+
export type TraceFileReader = (relativePath: string, maxBytes: number, length?: number, start?: number) => Promise<{
|
|
31
33
|
bytes: Buffer;
|
|
32
34
|
} | {
|
|
33
35
|
refused: string;
|
|
34
36
|
}>;
|
|
37
|
+
/**
|
|
38
|
+
* A live file the loader joined into stream `streamId`: the index it read the items by, and the
|
|
39
|
+
* cursor it read past when the stream holds only the items after one.
|
|
40
|
+
*/
|
|
41
|
+
export interface LiveItemsRead {
|
|
42
|
+
streamId: string;
|
|
43
|
+
index: LiveItemsIndex;
|
|
44
|
+
after?: LiveItemsCursor;
|
|
45
|
+
}
|
|
46
|
+
/** For a live file's index, the prefix of that file the caller holds already, if any. */
|
|
47
|
+
type LiveAfter = (index: LiveItemsIndex) => LiveItemsCursor | undefined;
|
|
35
48
|
/**
|
|
36
49
|
* The stream as a v1 run.json held it: its actor with every item and every artifact entry. A
|
|
37
50
|
* stream without an index is returned as it is (a v1 stream, or one with no actor). An indexed
|
|
@@ -44,6 +57,8 @@ export declare function loadStreamTrace(runPaths: PreparedOutputRoot, stream: Ru
|
|
|
44
57
|
interface LoadedTraces {
|
|
45
58
|
bundle: RunBundle;
|
|
46
59
|
refused: StreamTraceRefusedError[];
|
|
60
|
+
/** Each live file joined into a stream that loaded, in stream order. */
|
|
61
|
+
live: LiveItemsRead[];
|
|
47
62
|
}
|
|
48
63
|
/**
|
|
49
64
|
* Every stream of the bundle loaded through loadStreamTrace, one file at a time, and the refusal
|
|
@@ -53,6 +68,11 @@ interface LoadedTraces {
|
|
|
53
68
|
* receives each trace and live file's path and the bytes the stream was joined from, after they
|
|
54
69
|
* matched the index, so a caller scans exactly what it reads.
|
|
55
70
|
*
|
|
71
|
+
* `liveAfter` names, for a live file's index, the prefix the caller holds already (the Observer
|
|
72
|
+
* feed's cursor). The loader then reads and joins only the items past it, checked by liveItemsAfter
|
|
73
|
+
* without the indexed sha256, and `live` records the cursor; a cursor past the index or off the
|
|
74
|
+
* file's lines gets the whole prefix, checked as always.
|
|
75
|
+
*
|
|
56
76
|
* A live file that cannot be read may have been removed by the writer after `bundle` was read: it
|
|
57
77
|
* removes a superseded generation once run.json names the next one, and every live file once the
|
|
58
78
|
* run finished. The loader then reads run.json again, once, through the same reader, and loads
|
|
@@ -61,6 +81,7 @@ interface LoadedTraces {
|
|
|
61
81
|
export declare function loadStreamTraces(source: PreparedOutputRoot | TraceFileReader, bundle: RunBundle, options?: {
|
|
62
82
|
maxBytes?: number;
|
|
63
83
|
onTrace?: (path: string, bytes: Buffer) => void;
|
|
84
|
+
liveAfter?: LiveAfter;
|
|
64
85
|
}): Promise<LoadedTraces>;
|
|
65
86
|
/**
|
|
66
87
|
* A loaded bundle as a reader that shows a run presents it. A refused stream keeps run.json's
|
package/dist/run/stream-trace.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createHash } from "node:crypto";
|
|
|
8
8
|
import { RUN_BUNDLE_FILE, RUN_BUNDLE_SCHEMA, RUN_BUNDLE_V2_SCHEMA, } from "./bundle.js";
|
|
9
9
|
import { isRunBundle } from "./bundle-shape.js";
|
|
10
10
|
import { readContainedPrefix, readContainedRegularFile, refusalText, RUN_ARTIFACT_MAX_BYTES, } from "./contained-output.js";
|
|
11
|
-
import { isLiveItemsIndex, LIVE_ACTOR_SCHEMA, LIVE_ACTOR_V1_SCHEMA, liveItemsFromBytes, } from "./live-items.js";
|
|
11
|
+
import { isLiveItemsIndex, LIVE_ACTOR_SCHEMA, LIVE_ACTOR_V1_SCHEMA, liveItemsAfter, liveItemsFromBytes, } from "./live-items.js";
|
|
12
12
|
import { isRunStreamArtifact, isStreamTraceIndex, isStreamTracePath } from "./stream-shape.js";
|
|
13
13
|
import { isRecord } from "./type-guards.js";
|
|
14
14
|
/** A trace that does not match its index, or cannot be read. The message names the stream. */
|
|
@@ -87,10 +87,10 @@ export function splitStreamTraces(bundle) {
|
|
|
87
87
|
return { bundle: { ...bundle, schema: RUN_BUNDLE_SCHEMA, streams }, files };
|
|
88
88
|
}
|
|
89
89
|
function containedReader(runPaths) {
|
|
90
|
-
return async (relativePath, maxBytes, length) => {
|
|
90
|
+
return async (relativePath, maxBytes, length, start) => {
|
|
91
91
|
const read = length === undefined
|
|
92
92
|
? await readContainedRegularFile(runPaths, relativePath, maxBytes)
|
|
93
|
-
: await readContainedPrefix(runPaths, relativePath, length);
|
|
93
|
+
: await readContainedPrefix(runPaths, relativePath, length, start);
|
|
94
94
|
if (read.status === "missing")
|
|
95
95
|
return { refused: `${relativePath} is missing` };
|
|
96
96
|
if (read.status === "refused")
|
|
@@ -152,7 +152,7 @@ class LiveFileUnreadableError extends StreamTraceRefusedError {
|
|
|
152
152
|
* file's path and those bytes. A live actor with inline items (humanish.live-actor.v1) is returned
|
|
153
153
|
* as it is; bundleRefusal refuses one in a v3 run.json.
|
|
154
154
|
*/
|
|
155
|
-
async function readLiveItems(read, stream, maxBytes) {
|
|
155
|
+
async function readLiveItems(read, stream, maxBytes, liveAfter) {
|
|
156
156
|
const live = stream.liveActor;
|
|
157
157
|
if (live === undefined || isInlineLiveActor(live))
|
|
158
158
|
return { stream };
|
|
@@ -168,6 +168,15 @@ async function readLiveItems(read, stream, maxBytes) {
|
|
|
168
168
|
refuse("run.json holds in-progress items beside their live file");
|
|
169
169
|
if (index.bytes > maxBytes)
|
|
170
170
|
refuse(`${index.path} is indexed at ${index.bytes} bytes, more than the ${maxBytes} read`);
|
|
171
|
+
const { itemsFile: _index, ...summary } = live;
|
|
172
|
+
const after = liveAfter?.(index);
|
|
173
|
+
const tail = after === undefined ? undefined : await readLiveTail(read, stream, index, after);
|
|
174
|
+
if (tail !== undefined)
|
|
175
|
+
return {
|
|
176
|
+
stream: { ...stream, liveActor: { ...summary, items: tail.items } },
|
|
177
|
+
file: { path: index.path, bytes: tail.bytes },
|
|
178
|
+
read: { streamId: stream.id, index, after: after },
|
|
179
|
+
};
|
|
171
180
|
const file = await read(index.path, maxBytes, index.bytes);
|
|
172
181
|
if ("refused" in file)
|
|
173
182
|
throw new LiveFileUnreadableError(stream.id, file.refused);
|
|
@@ -175,17 +184,38 @@ async function readLiveItems(read, stream, maxBytes) {
|
|
|
175
184
|
const joined = liveItemsFromBytes(file.bytes.subarray(0, index.bytes), index);
|
|
176
185
|
if ("refused" in joined)
|
|
177
186
|
return refuse(joined.refused);
|
|
178
|
-
const { itemsFile: _index, ...summary } = live;
|
|
179
187
|
return {
|
|
180
188
|
stream: { ...stream, liveActor: { ...summary, items: joined.items } },
|
|
181
|
-
|
|
189
|
+
file: { path: index.path, bytes: file.bytes.subarray(0, index.bytes) },
|
|
190
|
+
read: { streamId: stream.id, index },
|
|
182
191
|
};
|
|
183
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* The items of the live file `index` describes past `after`, and the bytes they were read from;
|
|
195
|
+
* undefined when the cursor does not fit the index or the file's lines, so the caller reads the
|
|
196
|
+
* whole prefix. A cursor at the indexed end reads nothing. A file the reader cannot read throws
|
|
197
|
+
* LiveFileUnreadableError, as a whole read does.
|
|
198
|
+
*/
|
|
199
|
+
async function readLiveTail(read, stream, index, after) {
|
|
200
|
+
if (after.bytes === 0 ||
|
|
201
|
+
after.items > index.items ||
|
|
202
|
+
after.bytes > index.bytes ||
|
|
203
|
+
(after.items === index.items) !== (after.bytes === index.bytes))
|
|
204
|
+
return undefined;
|
|
205
|
+
if (after.bytes === index.bytes)
|
|
206
|
+
return { items: [], bytes: Buffer.alloc(0) };
|
|
207
|
+
const file = await read(index.path, index.bytes - after.bytes + 1, index.bytes, after.bytes - 1);
|
|
208
|
+
if ("refused" in file)
|
|
209
|
+
throw new LiveFileUnreadableError(stream.id, file.refused);
|
|
210
|
+
const tail = file.bytes.subarray(0, index.bytes - after.bytes + 1);
|
|
211
|
+
const joined = liveItemsAfter(tail, index, after);
|
|
212
|
+
return "refused" in joined ? undefined : { items: joined.items, bytes: tail.subarray(1) };
|
|
213
|
+
}
|
|
184
214
|
/**
|
|
185
215
|
* The stream with its trace and live items joined, and each file's path and the bytes it was
|
|
186
216
|
* joined from.
|
|
187
217
|
*/
|
|
188
|
-
async function readStreamTrace(read, stream, maxBytes) {
|
|
218
|
+
async function readStreamTrace(read, stream, maxBytes, liveAfter) {
|
|
189
219
|
const files = [];
|
|
190
220
|
let joined = stream;
|
|
191
221
|
if (stream.trace !== undefined) {
|
|
@@ -196,10 +226,10 @@ async function readStreamTrace(read, stream, maxBytes) {
|
|
|
196
226
|
joined = joinTrace(stream, file.bytes);
|
|
197
227
|
files.push({ path: stream.trace.path, bytes: file.bytes });
|
|
198
228
|
}
|
|
199
|
-
const live = await readLiveItems(read, joined, maxBytes);
|
|
200
|
-
if (live.
|
|
201
|
-
files.push(live.
|
|
202
|
-
return { stream: live.stream, files };
|
|
229
|
+
const live = await readLiveItems(read, joined, maxBytes, liveAfter);
|
|
230
|
+
if (live.file !== undefined)
|
|
231
|
+
files.push(live.file);
|
|
232
|
+
return { stream: live.stream, files, ...(live.read === undefined ? {} : { live: live.read }) };
|
|
203
233
|
}
|
|
204
234
|
/**
|
|
205
235
|
* The stream as a v1 run.json held it: its actor with every item and every artifact entry. A
|
|
@@ -249,6 +279,11 @@ function bundleRefusal(bundle, stream, owners) {
|
|
|
249
279
|
* receives each trace and live file's path and the bytes the stream was joined from, after they
|
|
250
280
|
* matched the index, so a caller scans exactly what it reads.
|
|
251
281
|
*
|
|
282
|
+
* `liveAfter` names, for a live file's index, the prefix the caller holds already (the Observer
|
|
283
|
+
* feed's cursor). The loader then reads and joins only the items past it, checked by liveItemsAfter
|
|
284
|
+
* without the indexed sha256, and `live` records the cursor; a cursor past the index or off the
|
|
285
|
+
* file's lines gets the whole prefix, checked as always.
|
|
286
|
+
*
|
|
252
287
|
* A live file that cannot be read may have been removed by the writer after `bundle` was read: it
|
|
253
288
|
* removes a superseded generation once run.json names the next one, and every live file once the
|
|
254
289
|
* run finished. The loader then reads run.json again, once, through the same reader, and loads
|
|
@@ -257,15 +292,15 @@ function bundleRefusal(bundle, stream, owners) {
|
|
|
257
292
|
export async function loadStreamTraces(source, bundle, options = {}) {
|
|
258
293
|
const read = typeof source === "function" ? source : containedReader(source);
|
|
259
294
|
const maxBytes = options.maxBytes ?? RUN_ARTIFACT_MAX_BYTES;
|
|
260
|
-
let loaded = await loadEveryStream(read, bundle, maxBytes);
|
|
295
|
+
let loaded = await loadEveryStream(read, bundle, maxBytes, options.liveAfter);
|
|
261
296
|
if (loaded.refused.some((error) => error instanceof LiveFileUnreadableError)) {
|
|
262
297
|
const current = await rereadBundle(read, bundle.runId);
|
|
263
298
|
if (current !== undefined)
|
|
264
|
-
loaded = await loadEveryStream(read, current, maxBytes);
|
|
299
|
+
loaded = await loadEveryStream(read, current, maxBytes, options.liveAfter);
|
|
265
300
|
}
|
|
266
301
|
for (const file of loaded.files)
|
|
267
302
|
options.onTrace?.(file.path, file.bytes);
|
|
268
|
-
return { bundle: loaded.bundle, refused: loaded.refused };
|
|
303
|
+
return { bundle: loaded.bundle, refused: loaded.refused, live: loaded.live };
|
|
269
304
|
}
|
|
270
305
|
/** run.json as it is now, when it is a run bundle of run `runId`. */
|
|
271
306
|
async function rereadBundle(read, runId) {
|
|
@@ -280,7 +315,7 @@ async function rereadBundle(read, runId) {
|
|
|
280
315
|
return undefined;
|
|
281
316
|
}
|
|
282
317
|
}
|
|
283
|
-
async function loadEveryStream(read, bundle, maxBytes) {
|
|
318
|
+
async function loadEveryStream(read, bundle, maxBytes, liveAfter) {
|
|
284
319
|
const owners = new Map();
|
|
285
320
|
for (const stream of bundle.streams)
|
|
286
321
|
if (isRecord(stream.trace)) {
|
|
@@ -290,13 +325,16 @@ async function loadEveryStream(read, bundle, maxBytes) {
|
|
|
290
325
|
const streams = [];
|
|
291
326
|
const refused = [];
|
|
292
327
|
const files = [];
|
|
328
|
+
const live = [];
|
|
293
329
|
for (const stream of bundle.streams) {
|
|
294
330
|
try {
|
|
295
331
|
const reason = bundleRefusal(bundle, stream, owners);
|
|
296
332
|
if (reason !== undefined)
|
|
297
333
|
throw new StreamTraceRefusedError(stream.id, reason);
|
|
298
|
-
const loaded = await readStreamTrace(read, stream, maxBytes);
|
|
334
|
+
const loaded = await readStreamTrace(read, stream, maxBytes, liveAfter);
|
|
299
335
|
files.push(...loaded.files);
|
|
336
|
+
if (loaded.live !== undefined)
|
|
337
|
+
live.push(loaded.live);
|
|
300
338
|
streams.push(loaded.stream);
|
|
301
339
|
}
|
|
302
340
|
catch (error) {
|
|
@@ -306,7 +344,7 @@ async function loadEveryStream(read, bundle, maxBytes) {
|
|
|
306
344
|
streams.push(stream);
|
|
307
345
|
}
|
|
308
346
|
}
|
|
309
|
-
return { bundle: { ...bundle, streams }, refused, files };
|
|
347
|
+
return { bundle: { ...bundle, streams }, refused, files, live };
|
|
310
348
|
}
|
|
311
349
|
/**
|
|
312
350
|
* A loaded bundle as a reader that shows a run presents it. A refused stream keeps run.json's
|
package/dist/run/streams.d.ts
CHANGED
|
@@ -175,6 +175,15 @@ export interface RunParticipantAssignment {
|
|
|
175
175
|
goal: string;
|
|
176
176
|
}>;
|
|
177
177
|
}
|
|
178
|
+
/** One file humanish copied onto a participant's desktop before it started (participants[].files). */
|
|
179
|
+
export interface RunDesktopFile {
|
|
180
|
+
/** Where it landed, relative to the desktop user's home folder. */
|
|
181
|
+
as: string;
|
|
182
|
+
/** Its size in bytes, confirmed on the desktop. */
|
|
183
|
+
size: number;
|
|
184
|
+
/** The sha256 of the bytes humanish copied, 64 hex characters. */
|
|
185
|
+
sha256: string;
|
|
186
|
+
}
|
|
178
187
|
export interface RunStream {
|
|
179
188
|
id: string;
|
|
180
189
|
simId: string;
|
|
@@ -232,6 +241,13 @@ export interface RunStream {
|
|
|
232
241
|
};
|
|
233
242
|
/** Truthful screen/window/viewport evidence for hosted desktop browser participants. */
|
|
234
243
|
desktopGeometry?: RunDesktopGeometry;
|
|
244
|
+
/** The files copied onto this participant's desktop before it started. Never their contents. */
|
|
245
|
+
desktopFiles?: RunDesktopFile[];
|
|
246
|
+
/**
|
|
247
|
+
* The participant's time zone (participants[].clock, else actor.clock): its turns gave the time
|
|
248
|
+
* in it, and its hosted browser ran in it. Absent when it had none.
|
|
249
|
+
*/
|
|
250
|
+
timeZone?: string;
|
|
235
251
|
terminal?: {
|
|
236
252
|
title: string;
|
|
237
253
|
format: "ansi" | "plain";
|