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
|
@@ -72,7 +72,7 @@ function sharedWorldSimulation(ctx, spec, index, view) {
|
|
|
72
72
|
updatedAt: args.run.createdAt,
|
|
73
73
|
...(args.plan.arrivals.declared
|
|
74
74
|
? {
|
|
75
|
-
arrival: arrivalRecord(spec.planned.startAfterMs, actorArrival(args.actorResults[index])),
|
|
75
|
+
arrival: arrivalRecord(spec.planned.schedule.startAfterMs, actorArrival(args.actorResults[index])),
|
|
76
76
|
}
|
|
77
77
|
: {}),
|
|
78
78
|
});
|
|
@@ -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.`,
|
|
@@ -104,7 +104,7 @@ export async function runExternalPublicPlane(ctx, live, inbox) {
|
|
|
104
104
|
}))
|
|
105
105
|
: undefined;
|
|
106
106
|
const followerResultsPromise = runOnSchedule(followerEntries, {
|
|
107
|
-
startAfterMs: ({ spec }) => spec.planned.startAfterMs,
|
|
107
|
+
startAfterMs: ({ spec }) => spec.planned.schedule.startAfterMs,
|
|
108
108
|
slots: Math.max(1, concurrency - 1),
|
|
109
109
|
now: ctx.now,
|
|
110
110
|
}, async ({ spec, index }, _position, { scheduledAt }) => ({
|
|
@@ -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),
|
|
@@ -23,6 +23,8 @@ export declare function planSharedWorldStudy(config: StudyConfig, input: {
|
|
|
23
23
|
readonly sandboxCeiling: SandboxCeiling;
|
|
24
24
|
/** How many sandboxes the operator's E2B plan runs at once (concurrentSandboxes). */
|
|
25
25
|
readonly concurrentSandboxes: ConcurrentSandboxes;
|
|
26
|
+
/** HUMANISH_CUA_MAX_CONCURRENCY (concurrencyCap), which may only lower the participants at once. */
|
|
27
|
+
readonly concurrencyCap?: number | undefined;
|
|
26
28
|
}): SharedWorldPlanResult;
|
|
27
29
|
/**
|
|
28
30
|
* The schedule lines a shared-world run prints before it creates anything, when the study declares
|
|
@@ -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))
|
|
@@ -98,7 +103,7 @@ export function planSharedWorldStudy(config, input) {
|
|
|
98
103
|
if (plane === undefined)
|
|
99
104
|
throw new Error("shared-world validation admitted a plane it cannot plan");
|
|
100
105
|
const sessionTimeoutMs = config.execution?.timeoutMs ?? defaultSessionTimeoutMs(plane, ceiling.ms);
|
|
101
|
-
const starts = startsOf(plane, config.execution?.concurrency, input.concurrentSandboxes, sessionTimeoutMs);
|
|
106
|
+
const starts = startsOf(plane, config.execution?.concurrency, { sandboxes: input.concurrentSandboxes, cap: input.concurrencyCap }, sessionTimeoutMs);
|
|
102
107
|
if (!starts.ok)
|
|
103
108
|
return refuse(invalid, starts.message, actor);
|
|
104
109
|
const { arrivals, warnings } = starts;
|
|
@@ -149,7 +154,7 @@ export function planSharedWorldStudy(config, input) {
|
|
|
149
154
|
* shared world needs two participants live together. On the external-public plane the host runs
|
|
150
155
|
* on a slot of its own and the others queue for the rest (external-public.ts).
|
|
151
156
|
*/
|
|
152
|
-
function startsOf(plane, declared,
|
|
157
|
+
function startsOf(plane, declared, limits, sessionMs) {
|
|
153
158
|
const n = plane.participants.length;
|
|
154
159
|
const atOnce = participantsAtOnce({
|
|
155
160
|
concurrency: boundedConcurrency(declared, n),
|
|
@@ -157,7 +162,7 @@ function startsOf(plane, declared, limit, sessionMs) {
|
|
|
157
162
|
participants: n,
|
|
158
163
|
alongside: plane.kind === "provisioned" ? 1 : 0,
|
|
159
164
|
minimum: 2,
|
|
160
|
-
},
|
|
165
|
+
}, limits);
|
|
161
166
|
if (!atOnce.ok)
|
|
162
167
|
return atOnce;
|
|
163
168
|
const hostIndex = plane.participants.findIndex((participant) => participant.host === true);
|
|
@@ -391,7 +391,7 @@ function runParticipants(plane, ctx, live, setup) {
|
|
|
391
391
|
const { commsEmail } = setup;
|
|
392
392
|
const baseActorDeps = participantRunDeps(ctx, live, ctx.scrubKnownValues);
|
|
393
393
|
const schedule = {
|
|
394
|
-
startAfterMs: (spec) => spec.planned.startAfterMs,
|
|
394
|
+
startAfterMs: (spec) => spec.planned.schedule.startAfterMs,
|
|
395
395
|
slots: Math.max(1, ctx.concurrency),
|
|
396
396
|
now,
|
|
397
397
|
};
|
package/dist/run/bundle.d.ts
CHANGED
|
@@ -511,8 +511,18 @@ export interface RunRerunLineage {
|
|
|
511
511
|
export interface ParticipantOutcomes {
|
|
512
512
|
/** Participants whose sessions reached a terminal state: the denominator for every count below. */
|
|
513
513
|
total: number;
|
|
514
|
-
/**
|
|
514
|
+
/**
|
|
515
|
+
* Recorded successful sessions; completion provenance depends on the actor and its evidence. A
|
|
516
|
+
* computer-use session that passed at its scheduled end or session cap counts here only when
|
|
517
|
+
* its participant declared that it reached the goal; otherwise it counts in `ranUntilEnd`.
|
|
518
|
+
*/
|
|
515
519
|
reachedGoal: number;
|
|
520
|
+
/**
|
|
521
|
+
* Passed at a scheduled end or a session cap without declaring that they reached the goal: the
|
|
522
|
+
* closing report said `DID NOT REACH THE GOAL.` or declared nothing. Present when above 0.
|
|
523
|
+
* Bundles written before this count existed tallied them under `reachedGoal`.
|
|
524
|
+
*/
|
|
525
|
+
ranUntilEnd?: number;
|
|
516
526
|
/** Stopped trying. A finding about the product. */
|
|
517
527
|
abandoned: number;
|
|
518
528
|
/** Interrupted before reaching the goal, including session, spend and provider limits. */
|
|
@@ -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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ActorCompletionReason, ActorStatus, ParticipantDeclaredOutcome } from "../actors/contract.js";
|
|
2
|
+
import type { ClockEnd } from "../actors/goal-source.js";
|
|
2
3
|
import type { ReviewSummary } from "./bundle.js";
|
|
3
4
|
export type Verdict = ReviewSummary["verdict"];
|
|
4
5
|
/** How a finished session ended, as the blocker rule reads it. */
|
|
@@ -6,7 +7,7 @@ export interface BlockerFacts {
|
|
|
6
7
|
completionReason: ActorCompletionReason;
|
|
7
8
|
/** A harness-owned stop condition ended the session: a matched stopWhen or a completed dwell window. */
|
|
8
9
|
stopConditionMatched: boolean;
|
|
9
|
-
/** What the participant declared
|
|
10
|
+
/** What the participant declared, when it declared an outcome (ActorTrace.declaredOutcome). */
|
|
10
11
|
declaredOutcome?: ParticipantDeclaredOutcome;
|
|
11
12
|
/** The route's reading of the closing report: it describes a blocker or asks for instructions. */
|
|
12
13
|
closingReportReadsBlocked: boolean;
|
|
@@ -22,9 +23,12 @@ export interface SessionEnding extends BlockerFacts {
|
|
|
22
23
|
*/
|
|
23
24
|
export declare function hollowCompletion(ending: SessionEnding): boolean;
|
|
24
25
|
/**
|
|
25
|
-
* The
|
|
26
|
-
* word and wins.
|
|
27
|
-
* session: a matched stopWhen is structured completion evidence and overrides
|
|
26
|
+
* The session ended goal_satisfied but the participant reported a blocker. A declared `reached`
|
|
27
|
+
* or `blocked` is its own word and wins. Otherwise the closing report is read, unless a stop
|
|
28
|
+
* condition ended the session: a matched stopWhen is structured completion evidence and overrides
|
|
29
|
+
* the text. A declared `not_reached` is read the same way: it ends the session as gave_up when
|
|
30
|
+
* the participant ends it, so here it comes from a closing report at a scheduled end or a session
|
|
31
|
+
* cap, and it does not say whether something stopped the participant.
|
|
28
32
|
*/
|
|
29
33
|
export declare function selfReportedBlocker(ending: BlockerFacts): boolean;
|
|
30
34
|
/** What a participant's pass reads: its session's end, or why it has none, and the two judgments above. */
|
|
@@ -40,6 +44,12 @@ export interface ParticipantFacts {
|
|
|
40
44
|
noEngagement: boolean;
|
|
41
45
|
/** selfReportedBlocker held for its session. */
|
|
42
46
|
selfReportedBlocker: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Its session ran until its scheduled end or session cap without the participant declaring it
|
|
49
|
+
* reached the goal (ranUntilClockEnd). A pass counts under `ranUntilEnd` in place of
|
|
50
|
+
* `reachedGoal`.
|
|
51
|
+
*/
|
|
52
|
+
ranUntil?: ClockEnd;
|
|
43
53
|
}
|
|
44
54
|
/** The participant facts its bundle and result can name. */
|
|
45
55
|
export interface ParticipantRecordFacts extends ParticipantFacts {
|
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 = {}) {
|
|
@@ -63,12 +68,12 @@ export function judgeParticipantRecords(participants, wording = {}) {
|
|
|
63
68
|
: `${participant.id ?? "Participant"}: ${participant.skippedReason ?? participant.sessionError ?? participant.reason ?? "did not pass"}`,
|
|
64
69
|
};
|
|
65
70
|
});
|
|
66
|
-
const
|
|
71
|
+
const ended = (participant) => participant.status !== undefined && !participant.inProgress;
|
|
72
|
+
const terminal = records.filter((_, index) => ended(participants[index]));
|
|
73
|
+
const terminalFacts = participants.filter(ended);
|
|
67
74
|
return {
|
|
68
75
|
participants: records,
|
|
69
|
-
tally: tallyParticipantOutcomes(terminal.map((participant) => participant.judgedStatus),
|
|
70
|
-
.filter((participant) => participant.status !== undefined && !participant.inProgress)
|
|
71
|
-
.map((participant) => participant.reportedFriction === true)),
|
|
76
|
+
tally: tallyParticipantOutcomes(terminal.map((participant) => participant.judgedStatus), terminalFacts.map((participant) => participant.reportedFriction === true), terminalFacts.map((participant) => participant.ranUntil !== undefined)),
|
|
72
77
|
};
|
|
73
78
|
}
|
|
74
79
|
function participantNotPassedMessage(participant, 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 };
|
package/dist/run/notes.js
CHANGED
|
@@ -21,7 +21,7 @@ const refuse = (code, message) => ({
|
|
|
21
21
|
});
|
|
22
22
|
/**
|
|
23
23
|
* The stream id a note's participant names: a stream id, or the participant's own id from the study
|
|
24
|
-
* (`
|
|
24
|
+
* (`support-lead`). Both kinds of id are matched, so a name that is one stream's id and another
|
|
25
25
|
* stream's study id is refused, naming both, as is a study id that two streams record. Otherwise
|
|
26
26
|
* the refusal names the run's participants by caption, with the id to type for each.
|
|
27
27
|
*/
|
package/dist/run/outcomes.d.ts
CHANGED
|
@@ -6,7 +6,13 @@ import type { ParticipantOutcomes, ReviewSummary, RunTaskFunnel } from "./bundle
|
|
|
6
6
|
* `total` but nowhere else, so the parts can never exceed the whole. */
|
|
7
7
|
export declare function tallyParticipantOutcomes(statuses: readonly ActorStatus[],
|
|
8
8
|
/** Per-participant: did this one report friction or a defect? Same order as `statuses`. */
|
|
9
|
-
reportedFriction?: readonly boolean[]
|
|
9
|
+
reportedFriction?: readonly boolean[],
|
|
10
|
+
/**
|
|
11
|
+
* Per-participant: did its session run until its scheduled end or session cap without it
|
|
12
|
+
* declaring that it reached the goal (ranUntilClockEnd)? Same order as `statuses`. Such a pass
|
|
13
|
+
* counts under `ranUntilEnd` in place of `reachedGoal`.
|
|
14
|
+
*/
|
|
15
|
+
ranUntilEnd?: readonly boolean[]): ParticipantOutcomes;
|
|
10
16
|
/** Roll per-participant funnels up into the study funnel. Undefined when nothing measured one. */
|
|
11
17
|
export declare function aggregateTaskFunnels(funnels: readonly TaskFunnel[]): RunTaskFunnel | undefined;
|
|
12
18
|
/** The funnel as one line, denominator on every number: `signup 2/2 · verify-email 1/2`. */
|
|
@@ -53,7 +59,7 @@ export declare function formatParticipantOutcomes(outcomes: ParticipantOutcomes,
|
|
|
53
59
|
export declare function participantOutcomeDetails(streams: readonly {
|
|
54
60
|
actor?: unknown;
|
|
55
61
|
status?: unknown;
|
|
56
|
-
}[]): ParticipantOutcomeDetail[];
|
|
62
|
+
}[], goalSourceOf?: (actor: unknown, recordedStatus: unknown) => CuaGoalSource | undefined): ParticipantOutcomeDetail[];
|
|
57
63
|
/** Refresh a CUA completion claim from recorded traces, leaving original evidence and enums intact. */
|
|
58
64
|
export declare function withCuaReviewProvenance(review: ReviewSummary, streams: readonly {
|
|
59
65
|
actor?: unknown;
|
package/dist/run/outcomes.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { ACTOR_STATUSES } from "../actors/contract.js";
|
|
2
|
-
import { cuaGoalSource, isCuaTrace, CUA_COMPLETION_NOTE, } from "../actors/goal-source.js";
|
|
3
|
-
import { actorEnding } from "../actors/stop-cause.js";
|
|
2
|
+
import { cuaEndpointSource, cuaGoalSource, isClockEnd, isCuaTrace, CUA_COMPLETION_NOTE, } from "../actors/goal-source.js";
|
|
3
|
+
import { actorEnding, actorStopCauseLabel } from "../actors/stop-cause.js";
|
|
4
4
|
import { plural } from "./text.js";
|
|
5
5
|
import { isNonNegativeSafeInteger, isRecord } from "./type-guards.js";
|
|
6
6
|
/** Tally participant outcomes from actor statuses. Statuses this does not recognise are counted in
|
|
7
7
|
* `total` but nowhere else, so the parts can never exceed the whole. */
|
|
8
8
|
export function tallyParticipantOutcomes(statuses,
|
|
9
9
|
/** Per-participant: did this one report friction or a defect? Same order as `statuses`. */
|
|
10
|
-
reportedFriction = []
|
|
10
|
+
reportedFriction = [],
|
|
11
|
+
/**
|
|
12
|
+
* Per-participant: did its session run until its scheduled end or session cap without it
|
|
13
|
+
* declaring that it reached the goal (ranUntilClockEnd)? Same order as `statuses`. Such a pass
|
|
14
|
+
* counts under `ranUntilEnd` in place of `reachedGoal`.
|
|
15
|
+
*/
|
|
16
|
+
ranUntilEnd = []) {
|
|
11
17
|
const tally = {
|
|
12
18
|
total: statuses.length,
|
|
13
19
|
reachedGoal: 0,
|
|
@@ -17,8 +23,11 @@ reportedFriction = []) {
|
|
|
17
23
|
harnessFailed: 0,
|
|
18
24
|
reportedFriction: reportedFriction.filter(Boolean).length,
|
|
19
25
|
};
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
let clockEnds = 0;
|
|
27
|
+
statuses.forEach((status, index) => {
|
|
28
|
+
if (status === "passed" && ranUntilEnd[index] === true)
|
|
29
|
+
clockEnds += 1;
|
|
30
|
+
else if (status === "passed")
|
|
22
31
|
tally.reachedGoal += 1;
|
|
23
32
|
else if (status === "abandoned")
|
|
24
33
|
tally.abandoned += 1;
|
|
@@ -28,8 +37,8 @@ reportedFriction = []) {
|
|
|
28
37
|
tally.blocked += 1;
|
|
29
38
|
else if (status === "failed")
|
|
30
39
|
tally.harnessFailed += 1;
|
|
31
|
-
}
|
|
32
|
-
return tally;
|
|
40
|
+
});
|
|
41
|
+
return clockEnds === 0 ? tally : { ...tally, ranUntilEnd: clockEnds };
|
|
33
42
|
}
|
|
34
43
|
/** Roll per-participant funnels up into the study funnel. Undefined when nothing measured one. */
|
|
35
44
|
export function aggregateTaskFunnels(funnels) {
|
|
@@ -86,15 +95,38 @@ export function formatRunTaskFunnel(funnel) {
|
|
|
86
95
|
})
|
|
87
96
|
.join(" · ");
|
|
88
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* "1 ran until its session cap", "2 ran until their scheduled end", grouped by clock end, most
|
|
100
|
+
* first. Clock ends the details cannot account for get one line that names both.
|
|
101
|
+
*/
|
|
102
|
+
function ranUntilPhrases(count, details) {
|
|
103
|
+
const words = (n, end) => n === 1 ? `1 ran until its ${end}` : `${n} ran until their ${end}`;
|
|
104
|
+
const ends = details.flatMap((entry) => (isClockEnd(entry.goalSource) ? [entry.goalSource] : []));
|
|
105
|
+
if (ends.length !== count)
|
|
106
|
+
return [words(count, "scheduled end or session cap")];
|
|
107
|
+
return ["session_cap", "scheduled_end"]
|
|
108
|
+
.map((end) => [end, ends.filter((entry) => entry === end).length])
|
|
109
|
+
.filter(([, n]) => n > 0)
|
|
110
|
+
.sort(([, m], [, n]) => n - m)
|
|
111
|
+
.map(([end, n]) => words(n, actorStopCauseLabel(end)));
|
|
112
|
+
}
|
|
89
113
|
function participantCompletionLine(outcomes, terminalCauses) {
|
|
90
|
-
const
|
|
114
|
+
const passed = terminalCauses.filter((entry) => entry.status === "passed");
|
|
115
|
+
// A bundle without `ranUntilEnd` either has no such clock end or predates the count and tallied
|
|
116
|
+
// its clock ends under reachedGoal. Those stay among its completions, and the breakdown names
|
|
117
|
+
// them, so a re-read never changes a recorded count.
|
|
118
|
+
const completions = outcomes.ranUntilEnd === undefined
|
|
119
|
+
? passed
|
|
120
|
+
: passed.filter((entry) => !isClockEnd(entry.goalSource));
|
|
91
121
|
const reported = completions.filter((entry) => entry.goalSource === "participant_report").length;
|
|
92
122
|
const matched = completions.filter((entry) => entry.goalSource === "condition_matched").length;
|
|
123
|
+
const clockEnds = completions.filter((entry) => isClockEnd(entry.goalSource));
|
|
93
124
|
let goalLine = outcomes.reachedGoal === 0
|
|
94
125
|
? `0/${outcomes.total} recorded completions`
|
|
95
126
|
: `${outcomes.reachedGoal}/${outcomes.total} reached the goal`;
|
|
96
127
|
if (outcomes.reachedGoal > 0 && terminalCauses.some((entry) => entry.goalSource !== undefined)) {
|
|
97
128
|
const count = `${outcomes.reachedGoal}/${outcomes.total}`;
|
|
129
|
+
const other = outcomes.reachedGoal - reported - matched - clockEnds.length;
|
|
98
130
|
goalLine =
|
|
99
131
|
completions.length !== outcomes.reachedGoal
|
|
100
132
|
? `${count} recorded completions (completion source unavailable)`
|
|
@@ -105,9 +137,8 @@ function participantCompletionLine(outcomes, terminalCauses) {
|
|
|
105
137
|
: `${count} recorded completions (${[
|
|
106
138
|
reported > 0 ? `${reported} participant-reported` : undefined,
|
|
107
139
|
matched > 0 ? `${matched} condition-matched` : undefined,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
: undefined,
|
|
140
|
+
...(clockEnds.length > 0 ? ranUntilPhrases(clockEnds.length, clockEnds) : []),
|
|
141
|
+
other > 0 ? `${other} other or unavailable source` : undefined,
|
|
111
142
|
]
|
|
112
143
|
.filter(Boolean)
|
|
113
144
|
.join(", ")})`;
|
|
@@ -124,6 +155,8 @@ export function sessionEndingInWords(trace) {
|
|
|
124
155
|
const source = cuaGoalSource(trace);
|
|
125
156
|
if (source === "participant_report")
|
|
126
157
|
return "reported reaching the goal";
|
|
158
|
+
if (isClockEnd(source))
|
|
159
|
+
return `ran until its ${actorStopCauseLabel(source)}`;
|
|
127
160
|
return source === "condition_matched" ? "met its completion condition" : "reached the goal";
|
|
128
161
|
}
|
|
129
162
|
case "turn_completed":
|
|
@@ -178,6 +211,10 @@ export function formatParticipantOutcomes(outcomes, terminalCauses = []) {
|
|
|
178
211
|
if (outcomes.total === 0)
|
|
179
212
|
return "no participants reached a terminal state";
|
|
180
213
|
const parts = [participantCompletionLine(outcomes, terminalCauses)];
|
|
214
|
+
// Passed at the clock without declaring that they reached the goal: next to the completions,
|
|
215
|
+
// and never counted among them.
|
|
216
|
+
if (outcomes.ranUntilEnd !== undefined && outcomes.ranUntilEnd > 0)
|
|
217
|
+
parts.push(...ranUntilPhrases(outcomes.ranUntilEnd, terminalCauses.filter((entry) => entry.status === "passed")));
|
|
181
218
|
// Detail may explain a recorded outcome, but must never change its count or invent a match
|
|
182
219
|
// between a tally and an incomplete set of traces.
|
|
183
220
|
const append = (count, statuses, fallback) => {
|
|
@@ -212,12 +249,12 @@ export function formatParticipantOutcomes(outcomes, terminalCauses = []) {
|
|
|
212
249
|
return parts.join(", ");
|
|
213
250
|
}
|
|
214
251
|
/** Presentation details tolerate optional legacy actor payloads without changing their tallies. */
|
|
215
|
-
export function participantOutcomeDetails(streams) {
|
|
252
|
+
export function participantOutcomeDetails(streams, goalSourceOf = cuaGoalSource) {
|
|
216
253
|
return streams.flatMap((stream) => {
|
|
217
254
|
if (!isRecord(stream.actor))
|
|
218
255
|
return [];
|
|
219
256
|
const actor = stream.actor;
|
|
220
|
-
const goalSource =
|
|
257
|
+
const goalSource = goalSourceOf(actor, stream.status);
|
|
221
258
|
if (!ACTOR_STATUSES.includes(String(actor.status))) {
|
|
222
259
|
return goalSource === "unavailable" ? [{ status: "passed", goalSource }] : [];
|
|
223
260
|
}
|
|
@@ -257,6 +294,9 @@ export function withCuaReviewProvenance(review, streams) {
|
|
|
257
294
|
const { reachedGoal, total } = review.participants;
|
|
258
295
|
const bareLine = `${reachedGoal}/${total} reached the goal`;
|
|
259
296
|
const qualifiedLine = participantCompletionLine(review.participants, details);
|
|
297
|
+
// Earlier releases qualified the count with the endpoint source (cuaEndpointSource), which
|
|
298
|
+
// reads a clock end as a participant report.
|
|
299
|
+
const endpointLine = participantCompletionLine(review.participants, participantOutcomeDetails(streams, cuaEndpointSource));
|
|
260
300
|
// A summary with no count gets it in a header. Earlier releases wrote a header in other words.
|
|
261
301
|
const header = `${plural(total, "participant")} took part: ${outcomes}.${tasks} `;
|
|
262
302
|
const olderHeader = `Run gate: ${review.verdict}. Participants: ${outcomes}.${tasks} Recorded summary: `;
|
|
@@ -266,7 +306,9 @@ export function withCuaReviewProvenance(review, streams) {
|
|
|
266
306
|
? review.summary.split(bareLine).join(qualifiedLine)
|
|
267
307
|
: review.summary.includes(qualifiedLine)
|
|
268
308
|
? review.summary
|
|
269
|
-
:
|
|
309
|
+
: review.summary.includes(endpointLine)
|
|
310
|
+
? review.summary.split(endpointLine).join(qualifiedLine)
|
|
311
|
+
: `${header}${review.summary}`;
|
|
270
312
|
return {
|
|
271
313
|
...review,
|
|
272
314
|
summary,
|
|
@@ -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
|