humanish 0.122.0 → 0.124.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/CONTEXT.md +2 -2
- package/README.md +33 -6
- package/dist/actors/computer-use/loop/trace.js +6 -1
- package/dist/actors/computer-use/openai-conversation.d.ts +0 -6
- package/dist/actors/computer-use/openai-conversation.js +0 -8
- package/dist/actors/computer-use/openai-provider.js +5 -8
- package/dist/analysis/automatic.js +2 -1
- package/dist/analysis/evidence.js +41 -21
- package/dist/cli/commands/study-run.js +9 -3
- package/dist/evidence/image.js +82 -3
- package/dist/feedback/derivative-text.d.ts +12 -0
- package/dist/feedback/derivative-text.js +130 -0
- package/dist/feedback/export-bundle.js +39 -136
- package/dist/feedback/export.js +36 -9
- package/dist/observer/artifact.js +2 -9
- package/dist/observer/data.d.ts +9 -2
- package/dist/observer/data.js +17 -2
- package/dist/observer/page-data.d.ts +5 -0
- package/dist/observer/page-data.js +21 -0
- package/dist/observer/participant-items.d.ts +45 -0
- package/dist/observer/participant-items.js +85 -0
- package/dist/observer/render.js +2 -1
- package/dist/observer/run-routes.js +75 -22
- package/dist/observer-app.html +4 -4
- package/dist/run/bundle-shape.js +2 -2
- package/dist/run/bundle.d.ts +5 -2
- package/dist/run/bundle.js +4 -1
- package/dist/run/detail.d.ts +6 -0
- package/dist/run/detail.js +14 -1
- package/dist/run/locate.js +3 -1
- package/dist/run/projection.d.ts +7 -0
- package/dist/run/projection.js +10 -0
- package/dist/run/run.js +9 -2
- package/dist/run/stored-runs.js +10 -4
- package/dist/run/stream-shape.d.ts +11 -1
- package/dist/run/stream-shape.js +26 -2
- package/dist/run/stream-trace.d.ts +68 -0
- package/dist/run/stream-trace.js +260 -0
- package/dist/run/streams.d.ts +20 -0
- package/dist/study/bundle-estimate.d.ts +48 -0
- package/dist/study/bundle-estimate.js +159 -0
- package/dist/study/doctor.js +4 -0
- package/dist/study/plan-types.d.ts +3 -0
- package/dist/study/plan.js +42 -2
- package/dist/study/preflight.js +8 -0
- package/dist/tui-app.js +87 -87
- package/dist/verify/artifacts.d.ts +17 -10
- package/dist/verify/artifacts.js +22 -13
- package/dist/verify/scan-limit.d.ts +6 -0
- package/dist/verify/scan-limit.js +25 -0
- package/dist/verify/verify.d.ts +1 -1
- package/dist/verify/verify.js +68 -18
- package/docs/architecture/github-feedback-loop.md +1 -1
- package/docs/architecture/observer.md +24 -5
- package/docs/contracts/README.md +1 -1
- package/docs/contracts/run-bundle.md +89 -6
- package/docs/contracts/schemas.md +14 -7
- package/docs/contracts/study-analysis.md +1 -1
- package/docs/decisions/0002-observer-is-one-self-contained-file.md +2 -1
- package/package.json +7 -4
package/dist/run/bundle-shape.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isCommsReceivingEvidence } from "../comms/receiving-evidence.js";
|
|
2
2
|
import { isCapturedGitState } from "./git-state.js";
|
|
3
|
-
import { PUBLIC_TARGET_CWD, REVIEW_SCHEMA, RUN_BUNDLE_SCHEMA, } from "./bundle.js";
|
|
3
|
+
import { PUBLIC_TARGET_CWD, REVIEW_SCHEMA, RUN_BUNDLE_SCHEMA, RUN_BUNDLE_V1_SCHEMA, } from "./bundle.js";
|
|
4
4
|
import { CLEANUP_SCHEMA, } from "./results.js";
|
|
5
5
|
import { isRunSimulationStatus, isRunStream, isRunStreamKind } from "./stream-shape.js";
|
|
6
6
|
import { isLocalEvidenceArtifactPath } from "./paths.js";
|
|
@@ -10,7 +10,7 @@ import { isSharedWorldEvidence } from "./shared-world-shape.js";
|
|
|
10
10
|
import { isNonNegativeSafeInteger, isPositiveSafeInteger, isRecord } from "./type-guards.js";
|
|
11
11
|
export function isRunBundle(value) {
|
|
12
12
|
return (isRecord(value) &&
|
|
13
|
-
value.schema === RUN_BUNDLE_SCHEMA &&
|
|
13
|
+
(value.schema === RUN_BUNDLE_SCHEMA || value.schema === RUN_BUNDLE_V1_SCHEMA) &&
|
|
14
14
|
(value.commsReceiving === undefined || isCommsReceivingEvidence(value.commsReceiving)) &&
|
|
15
15
|
(value.publication === undefined ||
|
|
16
16
|
(isRecord(value.publication) &&
|
package/dist/run/bundle.d.ts
CHANGED
|
@@ -5,7 +5,10 @@ import type { SharedWorldEvidence } from "./shared-world-evidence.js";
|
|
|
5
5
|
import { type RunStudyProvenance } from "./study-provenance.js";
|
|
6
6
|
import type { RunInterruptSignal } from "./status.js";
|
|
7
7
|
import type { RunSimulationStatus, RunStream, RunStreamKind } from "./streams.js";
|
|
8
|
-
|
|
8
|
+
/** The run bundle version this release writes: each stream's trace is a file run.json indexes. */
|
|
9
|
+
export declare const RUN_BUNDLE_SCHEMA = "humanish.run-bundle.v2";
|
|
10
|
+
/** The version 0.122.0 and earlier wrote, with every trace inline. It is still read. */
|
|
11
|
+
export declare const RUN_BUNDLE_V1_SCHEMA = "humanish.run-bundle.v1";
|
|
9
12
|
/** The run bundle file, relative to the run directory. */
|
|
10
13
|
export declare const RUN_BUNDLE_FILE = "run.json";
|
|
11
14
|
export declare const REVIEW_SCHEMA = "humanish.review.v1";
|
|
@@ -228,7 +231,7 @@ export interface RunBundle {
|
|
|
228
231
|
restrictions: ["real-communications"];
|
|
229
232
|
};
|
|
230
233
|
commsReceiving?: CommsReceivingEvidence;
|
|
231
|
-
schema: typeof RUN_BUNDLE_SCHEMA;
|
|
234
|
+
schema: typeof RUN_BUNDLE_SCHEMA | typeof RUN_BUNDLE_V1_SCHEMA;
|
|
232
235
|
runId: string;
|
|
233
236
|
mode: "dry-run" | "live";
|
|
234
237
|
simCount: number;
|
package/dist/run/bundle.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { captureGitState } from "./git-state.js";
|
|
3
3
|
import { studyFields } from "./study-provenance.js";
|
|
4
|
-
|
|
4
|
+
/** The run bundle version this release writes: each stream's trace is a file run.json indexes. */
|
|
5
|
+
export const RUN_BUNDLE_SCHEMA = "humanish.run-bundle.v2";
|
|
6
|
+
/** The version 0.122.0 and earlier wrote, with every trace inline. It is still read. */
|
|
7
|
+
export const RUN_BUNDLE_V1_SCHEMA = "humanish.run-bundle.v1";
|
|
5
8
|
/** The run bundle file, relative to the run directory. */
|
|
6
9
|
export const RUN_BUNDLE_FILE = "run.json";
|
|
7
10
|
export const REVIEW_SCHEMA = "humanish.review.v1";
|
package/dist/run/detail.d.ts
CHANGED
|
@@ -36,6 +36,12 @@ export interface RunParticipant {
|
|
|
36
36
|
thoughts?: number;
|
|
37
37
|
/** `null` when declared absent; absent when never recorded. Never coerced to 0. */
|
|
38
38
|
estimatedCostUsd?: number | null;
|
|
39
|
+
/**
|
|
40
|
+
* Why this participant's evidence cannot be shown: the loader refused its trace (a file that does
|
|
41
|
+
* not match run.json, or run.json items no trace file binds). The participant then has no
|
|
42
|
+
* thought or thought count.
|
|
43
|
+
*/
|
|
44
|
+
evidenceUnavailable?: string;
|
|
39
45
|
}
|
|
40
46
|
export interface RunDetail {
|
|
41
47
|
automaticAnalysis?: AutomaticAnalysisView;
|
package/dist/run/detail.js
CHANGED
|
@@ -16,6 +16,7 @@ import { savedCaption } from "./participant-caption.js";
|
|
|
16
16
|
import { recordedPersonaId, streamParticipantIdOf } from "./participant-records.js";
|
|
17
17
|
import { ContainedReadRefusedError, readContainedRegularFile, RUN_ARTIFACT_MAX_BYTES, } from "./contained-output.js";
|
|
18
18
|
import { isPathInside, resolvePhysicalCwd } from "./paths.js";
|
|
19
|
+
import { displayedTraces, loadStreamTraces } from "./stream-trace.js";
|
|
19
20
|
const RUN_DETAIL_SCHEMA = "humanish.run-detail.v1";
|
|
20
21
|
/**
|
|
21
22
|
* The latest reasoning a participant has recorded.
|
|
@@ -72,6 +73,9 @@ function participantFrom(stream, index, savedPersonaId) {
|
|
|
72
73
|
...(typeof trace.counts?.actions === "number" ? { actions: trace.counts.actions } : {}),
|
|
73
74
|
...(thoughts > 0 ? { thoughts } : {}),
|
|
74
75
|
...costOf(trace),
|
|
76
|
+
...(stream.traceUnavailable === undefined
|
|
77
|
+
? {}
|
|
78
|
+
: { evidenceUnavailable: stream.traceUnavailable }),
|
|
75
79
|
};
|
|
76
80
|
}
|
|
77
81
|
/**
|
|
@@ -103,6 +107,15 @@ function costOf(trace) {
|
|
|
103
107
|
return {};
|
|
104
108
|
}
|
|
105
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* The run's streams with their traces loaded through the loader and its bundle rules, as a screen
|
|
112
|
+
* shows them: a participant whose trace the loader refused carries the reason and none of the
|
|
113
|
+
* items run.json or a file supplied for it.
|
|
114
|
+
*/
|
|
115
|
+
async function displayedStreams(runPaths, streams, bundle) {
|
|
116
|
+
const loaded = await loadStreamTraces(runPaths, { ...bundle, streams });
|
|
117
|
+
return displayedTraces(loaded).streams;
|
|
118
|
+
}
|
|
106
119
|
/**
|
|
107
120
|
* Read one run's participants. Returns null when the run has no readable bundle yet, an ordinary
|
|
108
121
|
* state for a run that has just started, not a failure. Throws when run.json is there and cannot
|
|
@@ -138,7 +151,7 @@ export async function readRunDetail(cwdInput, runId) {
|
|
|
138
151
|
// Absent or torn: the run has not written one yet, or is writing it now.
|
|
139
152
|
return null;
|
|
140
153
|
}
|
|
141
|
-
const streams = Array.isArray(bundle.streams) ? bundle.streams : [];
|
|
154
|
+
const streams = await displayedStreams(runPaths, Array.isArray(bundle.streams) ? bundle.streams : [], bundle);
|
|
142
155
|
const observerAbsolute = path.join(runPaths.absoluteRunRoot, "observer", "index.html");
|
|
143
156
|
const automaticAnalysis = await readAutomaticAnalysis(cwd, runId);
|
|
144
157
|
return {
|
package/dist/run/locate.js
CHANGED
|
@@ -3,6 +3,7 @@ import { bindExistingRunArtifactPaths, isSafeRunIdSegment, resolveExistingRunDir
|
|
|
3
3
|
import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, ContainedReadRefusedError, LATEST_POINTER_MAX_BYTES, readContainedRegularFile, RUN_ARTIFACT_MAX_BYTES, } from "./contained-output.js";
|
|
4
4
|
import { RUN_BUNDLE_FILE } from "./bundle.js";
|
|
5
5
|
import { isRunBundle, isRunPointer } from "./bundle-shape.js";
|
|
6
|
+
import { loadRunTraces } from "./stream-trace.js";
|
|
6
7
|
import { readUnchangedFile } from "./evidence-files.js";
|
|
7
8
|
/** The run input that resolves through latest.json rather than naming a run directory. */
|
|
8
9
|
export const LATEST_RUN_ALIAS = "latest";
|
|
@@ -122,7 +123,8 @@ export async function loadRunBundlePrepared(cwdInput, runPaths) {
|
|
|
122
123
|
return null;
|
|
123
124
|
}
|
|
124
125
|
return {
|
|
125
|
-
|
|
126
|
+
// A trace that does not match its index throws StreamTraceRefusedError, naming the participant.
|
|
127
|
+
bundle: await loadRunTraces(runPaths, bundle),
|
|
126
128
|
bundlePath: path.relative(cwd, bundlePath),
|
|
127
129
|
runDir: runPaths.absoluteRunRoot,
|
|
128
130
|
};
|
package/dist/run/projection.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RunParticipant } from "./detail.js";
|
|
1
2
|
import type { RunIndexEntry } from "./run-index.js";
|
|
2
3
|
/** One study as the study list shows it. */
|
|
3
4
|
export interface StudyRollup {
|
|
@@ -72,6 +73,12 @@ export interface NormalizedThought {
|
|
|
72
73
|
/** True when the source text did not fit and was cut; shown, never silent. */
|
|
73
74
|
truncated: boolean;
|
|
74
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* What a screen quotes for a participant of a run detail: why its evidence is unavailable, else
|
|
78
|
+
* its latest thought. It lives here, beside normalizeThought, so the TUI bundle does not load the
|
|
79
|
+
* run-detail reader.
|
|
80
|
+
*/
|
|
81
|
+
export declare function participantNote(participant: Pick<RunParticipant, "evidenceUnavailable" | "thought">): string | undefined;
|
|
75
82
|
/**
|
|
76
83
|
* Prepare a participant's recorded reasoning summary for a fixed-width surface. Provider summaries
|
|
77
84
|
* arrive with markdown section leads (`**Thinking through setup**`) and hard newlines; a terminal
|
package/dist/run/projection.js
CHANGED
|
@@ -162,6 +162,16 @@ export function formatDuration(ms) {
|
|
|
162
162
|
const hours = Math.floor(minutes / 60);
|
|
163
163
|
return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`;
|
|
164
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* What a screen quotes for a participant of a run detail: why its evidence is unavailable, else
|
|
167
|
+
* its latest thought. It lives here, beside normalizeThought, so the TUI bundle does not load the
|
|
168
|
+
* run-detail reader.
|
|
169
|
+
*/
|
|
170
|
+
export function participantNote(participant) {
|
|
171
|
+
return participant.evidenceUnavailable === undefined
|
|
172
|
+
? participant.thought?.text
|
|
173
|
+
: `Evidence unavailable: ${participant.evidenceUnavailable}`;
|
|
174
|
+
}
|
|
165
175
|
/**
|
|
166
176
|
* Prepare a participant's recorded reasoning summary for a fixed-width surface. Provider summaries
|
|
167
177
|
* arrive with markdown section leads (`**Thinking through setup**`) and hard newlines; a terminal
|
package/dist/run/run.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
6
|
import { redactText } from "../evidence/redaction.js";
|
|
7
7
|
import { buildObserverData } from "../observer/data.js";
|
|
8
|
+
import { splitObserverData } from "../observer/participant-items.js";
|
|
8
9
|
import { renderObserver } from "../observer/render.js";
|
|
9
10
|
import { RUN_BUNDLE_FILE, PUBLIC_TARGET_CWD, buildRunSource, } from "./bundle.js";
|
|
10
11
|
import { judgeExecution, } from "./judge.js";
|
|
@@ -17,6 +18,7 @@ import { RunSecrets } from "./secrets.js";
|
|
|
17
18
|
import { RUN_STATUS_FILE, beginRunStatus, runStatusOutcome, } from "./status.js";
|
|
18
19
|
import { readRunJsonIfExists, runJsonValue } from "./locate.js";
|
|
19
20
|
import { hostSuspensionEvents, systemHostClock, } from "./host-suspension.js";
|
|
21
|
+
import { splitStreamTraces } from "./stream-trace.js";
|
|
20
22
|
/** An entry point called in a state that does not admit it. */
|
|
21
23
|
class RunLifecycleError extends Error {
|
|
22
24
|
name = "RunLifecycleError";
|
|
@@ -171,7 +173,12 @@ function runPublisher(args) {
|
|
|
171
173
|
cwd: PUBLIC_TARGET_CWD,
|
|
172
174
|
...(recorded === undefined ? {} : { outcome: recorded }),
|
|
173
175
|
});
|
|
174
|
-
|
|
176
|
+
// Each trace file first, so run.json never indexes a file that is not yet written. The review
|
|
177
|
+
// and the Observer data below read the whole in-memory bundle.
|
|
178
|
+
const split = splitStreamTraces(publicBundle);
|
|
179
|
+
for (const file of split.files)
|
|
180
|
+
await writeContainedOutputFile(paths, file.path, file.bytes);
|
|
181
|
+
await writeContainedOutputFile(paths, RUN_BUNDLE_FILE, json(split.bundle), "utf8");
|
|
175
182
|
await afterBundle(publicBundle);
|
|
176
183
|
// The run writes its own status.json; one it refuses renders as a missing one does.
|
|
177
184
|
const status = recorded === undefined
|
|
@@ -181,7 +188,7 @@ function runPublisher(args) {
|
|
|
181
188
|
await writeContainedOutputFile(paths, "review.md", options.renderReview(publicBundle, status), "utf8");
|
|
182
189
|
const events = publicBundle.events.map((event) => JSON.stringify(event)).join("\n");
|
|
183
190
|
await writeContainedOutputFile(paths, "events.ndjson", `${events}\n`, "utf8");
|
|
184
|
-
await writeContainedOutputFile(paths, "observer/observer-data.json", json(buildObserverData(publicBundle, undefined, status)), "utf8");
|
|
191
|
+
await writeContainedOutputFile(paths, "observer/observer-data.json", json(splitObserverData(buildObserverData(publicBundle, undefined, status)).shell), "utf8");
|
|
185
192
|
};
|
|
186
193
|
const snapshot = async (bundle) => {
|
|
187
194
|
await writeBundleFiles(bundle, undefined, async () => { });
|
package/dist/run/stored-runs.js
CHANGED
|
@@ -4,11 +4,12 @@ import { redactText } from "../evidence/redaction.js";
|
|
|
4
4
|
import { bindExistingRunArtifactPaths, RUNS_RELATIVE_ROOT, isSafeRunIdSegment, resolveRunsRoot, } from "./paths.js";
|
|
5
5
|
import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, refusalText, } from "./contained-output.js";
|
|
6
6
|
import { RUN_BUNDLE_FILE } from "./bundle.js";
|
|
7
|
-
import { isReviewSummary, isRunOutcome } from "./bundle-shape.js";
|
|
7
|
+
import { isReviewSummary, isRunBundle, isRunOutcome } from "./bundle-shape.js";
|
|
8
8
|
import { bundleDisplayFacts, displayedReview, runDisplay, } from "./display.js";
|
|
9
9
|
import { RUN_STATUS_FILE } from "./status.js";
|
|
10
10
|
import { readLatest, readRunJsonIfExists, resolveRunPath, runJsonValue } from "./locate.js";
|
|
11
11
|
import { withCuaReviewProvenance } from "./outcomes.js";
|
|
12
|
+
import { displayedTraces, loadStreamTraces } from "./stream-trace.js";
|
|
12
13
|
import { isNodeError, isRecord } from "./type-guards.js";
|
|
13
14
|
import { invalidRunStorageVerifyResult, verifyResolvedRun, } from "../verify/verify.js";
|
|
14
15
|
const RUNS_SCHEMA = "humanish.runs-result.v1";
|
|
@@ -174,9 +175,14 @@ export async function readReview(cwdInput, runInput) {
|
|
|
174
175
|
const bundle = runPaths
|
|
175
176
|
? runJsonValue(await readRunJsonIfExists(runPaths, RUN_BUNDLE_FILE))
|
|
176
177
|
: null;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
// The completion provenance reads each participant's items, loaded through the loader; a
|
|
179
|
+
// participant whose trace it refused counts without them.
|
|
180
|
+
const streams = runPaths && isRunBundle(bundle)
|
|
181
|
+
? displayedTraces(await loadStreamTraces(runPaths, bundle)).streams
|
|
182
|
+
: isRecord(bundle) && Array.isArray(bundle.streams)
|
|
183
|
+
? bundle.streams.filter(isRecord)
|
|
184
|
+
: undefined;
|
|
185
|
+
const projected = streams ? withCuaReviewProvenance(review, streams) : review;
|
|
180
186
|
const displayed = displayedBundle(bundle);
|
|
181
187
|
const { display } = runPaths ? await listedRunDisplay(runPaths, bundle) : {};
|
|
182
188
|
return {
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import { type RunSimulationStatus, type RunStream, type RunStreamKind } from "./streams.js";
|
|
1
|
+
import { type RunSimulationStatus, type RunStream, type RunStreamKind, type StreamTraceIndex } from "./streams.js";
|
|
2
2
|
export declare function isRunStream(value: unknown): value is RunStream;
|
|
3
|
+
export declare function isRunStreamArtifact(value: unknown): value is RunStream["artifacts"][number];
|
|
4
|
+
/**
|
|
5
|
+
* The trace file names the run writer produces: actor.json for one participant,
|
|
6
|
+
* actors/<stream>.json for more, actor-<surface>.json on the scripted route. A trace is source
|
|
7
|
+
* evidence, so an index naming any other file (the Observer page, which the public-safety scan
|
|
8
|
+
* reads as a regenerated projection) is refused by the shape check and by the loader.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isStreamTracePath(value: string): boolean;
|
|
11
|
+
/** A v2 stream's index of its trace file: a local path, its size and sha256, and what it holds. */
|
|
12
|
+
export declare function isStreamTraceIndex(value: unknown): value is StreamTraceIndex;
|
|
3
13
|
export declare function isRunSimulationStatus(value: unknown): value is RunSimulationStatus;
|
|
4
14
|
export declare function isRunStreamKind(value: unknown): value is RunStreamKind;
|
package/dist/run/stream-shape.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { desktopRecordingMetadataSchema, } from "../evidence/desktop-recording-types.js";
|
|
3
3
|
import { isLocalEvidenceArtifactPath } from "./paths.js";
|
|
4
4
|
import {} from "./streams.js";
|
|
5
|
-
import { isFiniteNumber, isPositiveFiniteNumber, isRecord } from "./type-guards.js";
|
|
5
|
+
import { isFiniteNumber, isNonNegativeSafeInteger, isPositiveFiniteNumber, isRecord, } from "./type-guards.js";
|
|
6
6
|
export function isRunStream(value) {
|
|
7
7
|
return (isRecord(value) &&
|
|
8
8
|
typeof value.id === "string" &&
|
|
@@ -21,6 +21,7 @@ export function isRunStream(value) {
|
|
|
21
21
|
(value.desktopGeometry === undefined || isRunDesktopGeometry(value.desktopGeometry)) &&
|
|
22
22
|
(value.recording === undefined || isRunDesktopRecording(value.recording)) &&
|
|
23
23
|
hasConsistentStreamGeometry(value) &&
|
|
24
|
+
(value.trace === undefined || isStreamTraceIndex(value.trace)) &&
|
|
24
25
|
Array.isArray(value.artifacts) &&
|
|
25
26
|
value.artifacts.every(isRunStreamArtifact) &&
|
|
26
27
|
hasConsistentRecordingArtifact(value.recording, value.artifacts));
|
|
@@ -120,7 +121,7 @@ function hasConsistentStreamGeometry(value) {
|
|
|
120
121
|
function isMeasuredSize(value) {
|
|
121
122
|
return (isRecord(value) && isPositiveFiniteNumber(value.width) && isPositiveFiniteNumber(value.height));
|
|
122
123
|
}
|
|
123
|
-
function isRunStreamArtifact(value) {
|
|
124
|
+
export function isRunStreamArtifact(value) {
|
|
124
125
|
return (isRecord(value) &&
|
|
125
126
|
typeof value.label === "string" &&
|
|
126
127
|
typeof value.path === "string" &&
|
|
@@ -134,6 +135,29 @@ function isRunStreamArtifact(value) {
|
|
|
134
135
|
value.kind === "filesystem" ||
|
|
135
136
|
value.kind === "recording"));
|
|
136
137
|
}
|
|
138
|
+
/** A name part of a trace file: a stream id or surface id that is a plain file name. */
|
|
139
|
+
const TRACE_NAME = "[A-Za-z0-9][A-Za-z0-9._-]{0,127}";
|
|
140
|
+
const TRACE_FILE = new RegExp(`^(?:actor|actor-${TRACE_NAME}|actors/${TRACE_NAME})\\.json$`);
|
|
141
|
+
/**
|
|
142
|
+
* The trace file names the run writer produces: actor.json for one participant,
|
|
143
|
+
* actors/<stream>.json for more, actor-<surface>.json on the scripted route. A trace is source
|
|
144
|
+
* evidence, so an index naming any other file (the Observer page, which the public-safety scan
|
|
145
|
+
* reads as a regenerated projection) is refused by the shape check and by the loader.
|
|
146
|
+
*/
|
|
147
|
+
export function isStreamTracePath(value) {
|
|
148
|
+
return TRACE_FILE.test(value);
|
|
149
|
+
}
|
|
150
|
+
/** A v2 stream's index of its trace file: a local path, its size and sha256, and what it holds. */
|
|
151
|
+
export function isStreamTraceIndex(value) {
|
|
152
|
+
return (isRecord(value) &&
|
|
153
|
+
typeof value.path === "string" &&
|
|
154
|
+
isStreamTracePath(value.path) &&
|
|
155
|
+
isNonNegativeSafeInteger(value.bytes) &&
|
|
156
|
+
typeof value.sha256 === "string" &&
|
|
157
|
+
/^[0-9a-f]{64}$/.test(value.sha256) &&
|
|
158
|
+
isNonNegativeSafeInteger(value.items) &&
|
|
159
|
+
isNonNegativeSafeInteger(value.screenshots));
|
|
160
|
+
}
|
|
137
161
|
export function isRunSimulationStatus(value) {
|
|
138
162
|
return (value === "queued" ||
|
|
139
163
|
value === "preparing" ||
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type RunBundle } from "./bundle.js";
|
|
2
|
+
import { type PreparedOutputRoot } from "./contained-output.js";
|
|
3
|
+
import type { RunStream } from "./streams.js";
|
|
4
|
+
/** A trace that does not match its index, or cannot be read. The message names the stream. */
|
|
5
|
+
export declare class StreamTraceRefusedError extends Error {
|
|
6
|
+
readonly streamId: string;
|
|
7
|
+
constructor(streamId: string, detail: string);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The run.json the run writes, and the trace files it writes first. Each stream with a finished
|
|
11
|
+
* actor keeps that actor without `items`, its artifacts other than screenshots, and an index of its
|
|
12
|
+
* trace file; the file is the actor trace with the stream's screenshot entries as `artifacts`. A
|
|
13
|
+
* stream already split (its actor read back from a v2 run.json without its items) keeps its index,
|
|
14
|
+
* since its file is on disk.
|
|
15
|
+
*/
|
|
16
|
+
export declare function splitStreamTraces(bundle: RunBundle): {
|
|
17
|
+
bundle: RunBundle;
|
|
18
|
+
files: {
|
|
19
|
+
path: string;
|
|
20
|
+
bytes: Buffer;
|
|
21
|
+
}[];
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Where a loader reads trace files from: the bytes at a run-relative path, at most `maxBytes`, or
|
|
25
|
+
* why it could not. Prepared run paths read through the contained reader; the Observer server
|
|
26
|
+
* passes a reader over its pinned run folder.
|
|
27
|
+
*/
|
|
28
|
+
export type TraceFileReader = (relativePath: string, maxBytes: number) => Promise<{
|
|
29
|
+
bytes: Buffer;
|
|
30
|
+
} | {
|
|
31
|
+
refused: string;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* The stream as a v1 run.json held it: its actor with every item and every artifact entry. A
|
|
35
|
+
* stream without an index is returned as it is (a v1 stream, or one with no actor). An indexed
|
|
36
|
+
* stream's file must have a trace file name (isStreamTracePath); it is read under `maxBytes` and
|
|
37
|
+
* checked against the index's byte count, sha256, item count and screenshot count, and any
|
|
38
|
+
* difference throws StreamTraceRefusedError. The rules that need the whole bundle (an unindexed v2
|
|
39
|
+
* actor, two participants indexing one file) are loadStreamTraces'.
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadStreamTrace(runPaths: PreparedOutputRoot, stream: RunStream, maxBytes?: number): Promise<RunStream>;
|
|
42
|
+
/**
|
|
43
|
+
* Every stream of the bundle loaded through loadStreamTrace, one file at a time, and the refusal
|
|
44
|
+
* of each stream that could not be (see bundleRefusal for the rules that need the whole bundle).
|
|
45
|
+
* Each refused stream is kept as run.json holds it; a reader that shows the run passes the result
|
|
46
|
+
* to displayedTraces. `source` is the run's prepared paths, or a TraceFileReader. `onTrace`
|
|
47
|
+
* receives each trace file's path and the bytes the stream was joined from, after they matched
|
|
48
|
+
* the index, so a caller scans exactly what it reads.
|
|
49
|
+
*/
|
|
50
|
+
export declare function loadStreamTraces(source: PreparedOutputRoot | TraceFileReader, bundle: RunBundle, options?: {
|
|
51
|
+
maxBytes?: number;
|
|
52
|
+
onTrace?: (path: string, bytes: Buffer) => void;
|
|
53
|
+
}): Promise<{
|
|
54
|
+
bundle: RunBundle;
|
|
55
|
+
refused: StreamTraceRefusedError[];
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* A loaded bundle as a reader that shows a run presents it. A refused stream keeps run.json's
|
|
59
|
+
* summary, shows none of the items run.json or a file supplied for it (its actor's items, its
|
|
60
|
+
* in-progress items, its screenshot entries), and carries `traceUnavailable` with the reason; a
|
|
61
|
+
* warn event on its timeline says the same.
|
|
62
|
+
*/
|
|
63
|
+
export declare function displayedTraces(loaded: {
|
|
64
|
+
bundle: RunBundle;
|
|
65
|
+
refused: readonly StreamTraceRefusedError[];
|
|
66
|
+
}): RunBundle;
|
|
67
|
+
/** The bundle with every stream loaded; the first refusal throws. */
|
|
68
|
+
export declare function loadRunTraces(source: PreparedOutputRoot | TraceFileReader, bundle: RunBundle, maxBytes?: number): Promise<RunBundle>;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// Where a participant's trace lives. A v1 run.json holds every stream's trace items and screenshot
|
|
2
|
+
// entries inline. From v2 the run writer moves them into one file per stream and leaves an index
|
|
3
|
+
// in run.json, so run.json stays a summary whatever the length of the sessions. Every reader that
|
|
4
|
+
// needs the items loads them here, which accepts both versions and checks a file against its index.
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { RUN_BUNDLE_SCHEMA } from "./bundle.js";
|
|
7
|
+
import { readContainedRegularFile, refusalText, RUN_ARTIFACT_MAX_BYTES, } from "./contained-output.js";
|
|
8
|
+
import { isRunStreamArtifact, isStreamTraceIndex, isStreamTracePath } from "./stream-shape.js";
|
|
9
|
+
import { isRecord } from "./type-guards.js";
|
|
10
|
+
/** A trace that does not match its index, or cannot be read. The message names the stream. */
|
|
11
|
+
export class StreamTraceRefusedError extends Error {
|
|
12
|
+
streamId;
|
|
13
|
+
constructor(streamId, detail) {
|
|
14
|
+
super(`Participant ${streamId}'s trace was not read: ${detail}. A trace file must stay as the run wrote it; restore the run folder from a copy.`);
|
|
15
|
+
this.streamId = streamId;
|
|
16
|
+
this.name = "StreamTraceRefusedError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
20
|
+
const isScreenshot = (artifact) => artifact.kind === "screenshot";
|
|
21
|
+
/**
|
|
22
|
+
* The file a trace path names on any file system. Trace file names are ASCII (isStreamTracePath),
|
|
23
|
+
* so lower case folds them as a case-insensitive file system does: actors/Case.json and
|
|
24
|
+
* actors/case.json are one file there.
|
|
25
|
+
*/
|
|
26
|
+
const traceFileKey = (tracePath) => tracePath.toLowerCase();
|
|
27
|
+
/** Names of the form actors/<n>.json, which the splitter gives by position and never reuses. */
|
|
28
|
+
const POSITION_TRACE_FILE = /^actors\/\d+\.json$/;
|
|
29
|
+
/**
|
|
30
|
+
* The stream's actor trace artifact (actor.json, actors/<stream>.json, actor-<surface>.json) when
|
|
31
|
+
* no earlier stream claimed it (compared by traceFileKey), else actors/<position>.json. The
|
|
32
|
+
* position names are a namespace of their own: an artifact named that way is not reused, so two
|
|
33
|
+
* streams never get one file. Another trace artifact (the scripted route's traces/<surface>.json
|
|
34
|
+
* holds its native step trace) keeps its own file.
|
|
35
|
+
*/
|
|
36
|
+
function tracePath(stream, index, claimed) {
|
|
37
|
+
const own = stream.artifacts.find((artifact) => artifact.kind === "trace" &&
|
|
38
|
+
isStreamTracePath(artifact.path) &&
|
|
39
|
+
!POSITION_TRACE_FILE.test(artifact.path))?.path;
|
|
40
|
+
return own !== undefined && !claimed.has(traceFileKey(own)) ? own : `actors/${index + 1}.json`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The run.json the run writes, and the trace files it writes first. Each stream with a finished
|
|
44
|
+
* actor keeps that actor without `items`, its artifacts other than screenshots, and an index of its
|
|
45
|
+
* trace file; the file is the actor trace with the stream's screenshot entries as `artifacts`. A
|
|
46
|
+
* stream already split (its actor read back from a v2 run.json without its items) keeps its index,
|
|
47
|
+
* since its file is on disk.
|
|
48
|
+
*/
|
|
49
|
+
export function splitStreamTraces(bundle) {
|
|
50
|
+
const files = [];
|
|
51
|
+
const claimed = new Set();
|
|
52
|
+
for (const stream of bundle.streams)
|
|
53
|
+
if (stream.trace)
|
|
54
|
+
claimed.add(traceFileKey(stream.trace.path));
|
|
55
|
+
const streams = bundle.streams.map((stream, index) => {
|
|
56
|
+
const actor = stream.actor;
|
|
57
|
+
if (actor === undefined || !Array.isArray(actor.items))
|
|
58
|
+
return stream;
|
|
59
|
+
const { trace: _previous, ...rest } = stream;
|
|
60
|
+
const filePath = tracePath(rest, index, claimed);
|
|
61
|
+
// Only a stream already split that moved position can hold this stream's position name.
|
|
62
|
+
if (claimed.has(traceFileKey(filePath)))
|
|
63
|
+
throw new Error(`Two participants' traces would both be written to ${filePath}.`);
|
|
64
|
+
claimed.add(traceFileKey(filePath));
|
|
65
|
+
const screenshots = stream.artifacts.filter(isScreenshot);
|
|
66
|
+
const bytes = Buffer.from(`${JSON.stringify({ ...actor, artifacts: screenshots }, null, 2)}\n`);
|
|
67
|
+
files.push({ path: filePath, bytes });
|
|
68
|
+
const { items, ...summary } = actor;
|
|
69
|
+
return {
|
|
70
|
+
...rest,
|
|
71
|
+
// run.json's actor is the summary: every field of the trace except its items.
|
|
72
|
+
actor: summary,
|
|
73
|
+
artifacts: stream.artifacts.filter((artifact) => !isScreenshot(artifact)),
|
|
74
|
+
trace: {
|
|
75
|
+
path: filePath,
|
|
76
|
+
bytes: bytes.length,
|
|
77
|
+
sha256: sha256(bytes),
|
|
78
|
+
items: items.length,
|
|
79
|
+
screenshots: screenshots.length,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
return { bundle: { ...bundle, schema: RUN_BUNDLE_SCHEMA, streams }, files };
|
|
84
|
+
}
|
|
85
|
+
function containedReader(runPaths) {
|
|
86
|
+
return async (relativePath, maxBytes) => {
|
|
87
|
+
const read = await readContainedRegularFile(runPaths, relativePath, maxBytes);
|
|
88
|
+
if (read.status === "missing")
|
|
89
|
+
return { refused: `${relativePath} is missing` };
|
|
90
|
+
if (read.status === "refused")
|
|
91
|
+
return { refused: refusalText(relativePath, read) };
|
|
92
|
+
return { bytes: read.bytes };
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The stream from the bytes of its trace file: its actor with the file's items, and its artifacts
|
|
97
|
+
* followed by the file's screenshot entries, without the index.
|
|
98
|
+
*/
|
|
99
|
+
function joinTrace(stream, bytes) {
|
|
100
|
+
const index = stream.trace;
|
|
101
|
+
const refuse = (detail) => {
|
|
102
|
+
throw new StreamTraceRefusedError(stream.id, detail);
|
|
103
|
+
};
|
|
104
|
+
if (bytes.length !== index.bytes)
|
|
105
|
+
refuse(`${index.path} holds ${bytes.length} bytes and run.json records ${index.bytes} bytes`);
|
|
106
|
+
if (sha256(bytes) !== index.sha256)
|
|
107
|
+
refuse(`${index.path} does not match the sha256 run.json records for it`);
|
|
108
|
+
let value;
|
|
109
|
+
try {
|
|
110
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
refuse(`${index.path} is not JSON`);
|
|
114
|
+
}
|
|
115
|
+
if (!isRecord(value) || !Array.isArray(value.items) || !Array.isArray(value.artifacts))
|
|
116
|
+
return refuse(`${index.path} has no items or artifacts list`);
|
|
117
|
+
const artifacts = value.artifacts;
|
|
118
|
+
if (!artifacts.every((artifact) => isRunStreamArtifact(artifact) && isScreenshot(artifact)))
|
|
119
|
+
refuse(`${index.path} lists an artifact that is not a screenshot entry`);
|
|
120
|
+
if (value.items.length !== index.items || artifacts.length !== index.screenshots)
|
|
121
|
+
refuse(`${index.path} holds ${value.items.length} items and ${artifacts.length} screenshots and run.json records ${index.items} and ${index.screenshots}`);
|
|
122
|
+
const { trace: _index, ...rest } = stream;
|
|
123
|
+
return {
|
|
124
|
+
...rest,
|
|
125
|
+
actor: { ...stream.actor, items: value.items },
|
|
126
|
+
artifacts: [...stream.artifacts, ...artifacts],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Refuses an index the shape check would refuse, naming a file that is not a trace file name. */
|
|
130
|
+
function assertIndex(stream) {
|
|
131
|
+
const index = stream.trace;
|
|
132
|
+
if (isRecord(index) && typeof index.path === "string" && !isStreamTracePath(index.path))
|
|
133
|
+
throw new StreamTraceRefusedError(stream.id, `run.json indexes ${index.path}, which is not a trace file name (actor.json, actors/<stream>.json or actor-<surface>.json)`);
|
|
134
|
+
if (!isStreamTraceIndex(index) || stream.actor === undefined)
|
|
135
|
+
throw new StreamTraceRefusedError(stream.id, "run.json records a malformed trace index");
|
|
136
|
+
if (stream.actor.items !== undefined)
|
|
137
|
+
throw new StreamTraceRefusedError(stream.id, "run.json holds items beside its trace index");
|
|
138
|
+
}
|
|
139
|
+
/** The stream with its trace joined, and the bytes it was joined from when it has an index. */
|
|
140
|
+
async function readStreamTrace(read, stream, maxBytes) {
|
|
141
|
+
if (stream.trace === undefined)
|
|
142
|
+
return { stream };
|
|
143
|
+
assertIndex(stream);
|
|
144
|
+
const file = await read(stream.trace.path, maxBytes);
|
|
145
|
+
if ("refused" in file)
|
|
146
|
+
throw new StreamTraceRefusedError(stream.id, file.refused);
|
|
147
|
+
return { stream: joinTrace(stream, file.bytes), bytes: file.bytes };
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The stream as a v1 run.json held it: its actor with every item and every artifact entry. A
|
|
151
|
+
* stream without an index is returned as it is (a v1 stream, or one with no actor). An indexed
|
|
152
|
+
* stream's file must have a trace file name (isStreamTracePath); it is read under `maxBytes` and
|
|
153
|
+
* checked against the index's byte count, sha256, item count and screenshot count, and any
|
|
154
|
+
* difference throws StreamTraceRefusedError. The rules that need the whole bundle (an unindexed v2
|
|
155
|
+
* actor, two participants indexing one file) are loadStreamTraces'.
|
|
156
|
+
*/
|
|
157
|
+
export async function loadStreamTrace(runPaths, stream, maxBytes = RUN_ARTIFACT_MAX_BYTES) {
|
|
158
|
+
return (await readStreamTrace(containedReader(runPaths), stream, maxBytes)).stream;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Why a stream of this bundle cannot be read, before any file is. A trace file belongs to one
|
|
162
|
+
* participant, so every stream indexing a path another stream indexes, compared as a
|
|
163
|
+
* case-insensitive file system compares them, is refused. In v2 the writer
|
|
164
|
+
* indexes every actor on every write, mid-run flushes included, so a v2 actor without an index lost
|
|
165
|
+
* its trace file, and inline items never come from the writer. A run still writing carries its
|
|
166
|
+
* participants' items so far as `liveActor` (humanish.live-actor.v1), which no index binds; a
|
|
167
|
+
* run.json whose outcome says the run finished never does, since the final write replaces it.
|
|
168
|
+
*/
|
|
169
|
+
function bundleRefusal(bundle, stream, owners) {
|
|
170
|
+
const shared = isRecord(stream.trace)
|
|
171
|
+
? owners.get(traceFileKey(String(stream.trace.path)))
|
|
172
|
+
: undefined;
|
|
173
|
+
if (shared !== undefined && shared.length > 1)
|
|
174
|
+
return `run.json indexes ${String(stream.trace.path)} for participants ${shared.join(", ")} (paths that differ only in case name one file on some file systems), and a trace file belongs to one participant`;
|
|
175
|
+
if (bundle.schema !== RUN_BUNDLE_SCHEMA)
|
|
176
|
+
return undefined;
|
|
177
|
+
if (stream.actor !== undefined && stream.trace === undefined)
|
|
178
|
+
return "run.json holds its actor and names no trace file";
|
|
179
|
+
if (stream.liveActor !== undefined && bundle.outcome?.state === "finished")
|
|
180
|
+
return "run.json records a finished run and still holds in-progress items";
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Every stream of the bundle loaded through loadStreamTrace, one file at a time, and the refusal
|
|
185
|
+
* of each stream that could not be (see bundleRefusal for the rules that need the whole bundle).
|
|
186
|
+
* Each refused stream is kept as run.json holds it; a reader that shows the run passes the result
|
|
187
|
+
* to displayedTraces. `source` is the run's prepared paths, or a TraceFileReader. `onTrace`
|
|
188
|
+
* receives each trace file's path and the bytes the stream was joined from, after they matched
|
|
189
|
+
* the index, so a caller scans exactly what it reads.
|
|
190
|
+
*/
|
|
191
|
+
export async function loadStreamTraces(source, bundle, options = {}) {
|
|
192
|
+
const read = typeof source === "function" ? source : containedReader(source);
|
|
193
|
+
const owners = new Map();
|
|
194
|
+
for (const stream of bundle.streams)
|
|
195
|
+
if (isRecord(stream.trace)) {
|
|
196
|
+
const key = traceFileKey(String(stream.trace.path));
|
|
197
|
+
owners.set(key, [...(owners.get(key) ?? []), stream.id]);
|
|
198
|
+
}
|
|
199
|
+
const streams = [];
|
|
200
|
+
const refused = [];
|
|
201
|
+
for (const stream of bundle.streams) {
|
|
202
|
+
try {
|
|
203
|
+
const reason = bundleRefusal(bundle, stream, owners);
|
|
204
|
+
if (reason !== undefined)
|
|
205
|
+
throw new StreamTraceRefusedError(stream.id, reason);
|
|
206
|
+
const loaded = await readStreamTrace(read, stream, options.maxBytes ?? RUN_ARTIFACT_MAX_BYTES);
|
|
207
|
+
if (loaded.bytes !== undefined)
|
|
208
|
+
options.onTrace?.(stream.trace.path, loaded.bytes);
|
|
209
|
+
streams.push(loaded.stream);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (!(error instanceof StreamTraceRefusedError))
|
|
213
|
+
throw error;
|
|
214
|
+
refused.push(error);
|
|
215
|
+
streams.push(stream);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { bundle: { ...bundle, streams }, refused };
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* A loaded bundle as a reader that shows a run presents it. A refused stream keeps run.json's
|
|
222
|
+
* summary, shows none of the items run.json or a file supplied for it (its actor's items, its
|
|
223
|
+
* in-progress items, its screenshot entries), and carries `traceUnavailable` with the reason; a
|
|
224
|
+
* warn event on its timeline says the same.
|
|
225
|
+
*/
|
|
226
|
+
export function displayedTraces(loaded) {
|
|
227
|
+
const reasons = new Map(loaded.refused.map((error) => [error.streamId, error.message]));
|
|
228
|
+
if (reasons.size === 0)
|
|
229
|
+
return loaded.bundle;
|
|
230
|
+
const streams = loaded.bundle.streams.map((stream) => {
|
|
231
|
+
const reason = reasons.get(stream.id);
|
|
232
|
+
if (reason === undefined)
|
|
233
|
+
return stream;
|
|
234
|
+
const { liveActor: _live, trace: _index, actor, ...rest } = stream;
|
|
235
|
+
return {
|
|
236
|
+
...rest,
|
|
237
|
+
...(actor === undefined ? {} : { actor: { ...actor, items: [] } }),
|
|
238
|
+
artifacts: stream.artifacts.filter((artifact) => !isScreenshot(artifact)),
|
|
239
|
+
traceUnavailable: reason,
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
const events = loaded.bundle.streams
|
|
243
|
+
.filter((stream) => reasons.has(stream.id))
|
|
244
|
+
.map((stream) => ({
|
|
245
|
+
id: `event-trace-unavailable-${stream.id}`,
|
|
246
|
+
at: stream.updatedAt,
|
|
247
|
+
level: "warn",
|
|
248
|
+
type: "trace.unavailable",
|
|
249
|
+
message: reasons.get(stream.id),
|
|
250
|
+
streamId: stream.id,
|
|
251
|
+
}));
|
|
252
|
+
return { ...loaded.bundle, streams, events: [...loaded.bundle.events, ...events] };
|
|
253
|
+
}
|
|
254
|
+
/** The bundle with every stream loaded; the first refusal throws. */
|
|
255
|
+
export async function loadRunTraces(source, bundle, maxBytes) {
|
|
256
|
+
const loaded = await loadStreamTraces(source, bundle, maxBytes === undefined ? {} : { maxBytes });
|
|
257
|
+
if (loaded.refused[0])
|
|
258
|
+
throw loaded.refused[0];
|
|
259
|
+
return loaded.bundle;
|
|
260
|
+
}
|