humanish 0.115.0 → 0.116.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 +22 -0
- package/README.md +7 -6
- package/TELEMETRY.md +6 -0
- package/dist/actors/codex/app-server-ui.js +11 -4
- package/dist/actors/computer-use/loop/debrief.js +61 -27
- package/dist/actors/scripted-browser/steps.js +6 -5
- package/dist/analysis/analysis-limits.d.ts +35 -0
- package/dist/analysis/analysis-limits.js +41 -0
- package/dist/analysis/validation.js +14 -21
- package/dist/cli/commands/analyze.js +2 -1
- package/dist/cli/commands/notes.js +2 -1
- package/dist/cli/commands/run-signals.js +5 -1
- package/dist/cli/commands/runs.js +2 -1
- package/dist/cli/commands/setup.js +3 -3
- package/dist/cli/commands/study-format.d.ts +5 -2
- package/dist/cli/commands/study-format.js +29 -9
- package/dist/cli/commands/study-route-computer-use.js +2 -2
- package/dist/cli/commands/study-route-scripted.js +1 -1
- package/dist/cli/commands/tui.js +59 -17
- package/dist/cli/doctor.js +9 -2
- package/dist/cli/findings-text.d.ts +5 -0
- package/dist/cli/findings-text.js +169 -0
- package/dist/cli/findings.d.ts +0 -4
- package/dist/cli/findings.js +5 -167
- package/dist/cli/io.js +6 -1
- package/dist/cli/update-check.js +4 -1
- package/dist/comms/connections.js +4 -3
- package/dist/comms/setup.js +18 -12
- package/dist/evidence/redaction.js +9 -1
- package/dist/evidence/secret-scrub.d.ts +15 -5
- package/dist/evidence/secret-scrub.js +67 -9
- package/dist/feedback/draft.js +1 -1
- package/dist/feedback/export-bundle.js +18 -14
- package/dist/feedback/feedback.js +25 -7
- package/dist/guest/desktop-keys.js +59 -5
- package/dist/keys/key-resolution.d.ts +9 -0
- package/dist/keys/key-resolution.js +60 -48
- package/dist/keys/key-status.d.ts +2 -0
- package/dist/keys/key-status.js +9 -16
- package/dist/observer/pinned-files.js +1 -1
- package/dist/observer/render.js +3 -2
- package/dist/observer-app.html +7 -7
- package/dist/routes/computer-use/bundle-parts.d.ts +4 -4
- package/dist/routes/computer-use/bundle-parts.js +2 -2
- package/dist/routes/computer-use/bundle.d.ts +4 -5
- package/dist/routes/computer-use/bundle.js +1 -3
- package/dist/routes/computer-use/fanout-records.js +10 -11
- package/dist/routes/computer-use/live-phase.d.ts +1 -1
- package/dist/routes/computer-use/live-phase.js +0 -1
- package/dist/routes/computer-use/participant-execution.d.ts +3 -2
- package/dist/routes/computer-use/participant-runs.d.ts +6 -2
- package/dist/routes/computer-use/participant-runs.js +9 -5
- package/dist/routes/computer-use/plan.d.ts +3 -0
- package/dist/routes/computer-use/plan.js +14 -11
- package/dist/routes/computer-use/rerun-selection.js +2 -2
- package/dist/routes/computer-use/result.js +3 -6
- package/dist/routes/computer-use/setup.d.ts +4 -6
- package/dist/routes/computer-use/setup.js +2 -6
- package/dist/routes/computer-use/single-bundle.d.ts +4 -5
- package/dist/routes/computer-use/single-bundle.js +22 -25
- package/dist/routes/computer-use/subject-projection.d.ts +4 -8
- package/dist/routes/computer-use/subject-projection.js +9 -31
- package/dist/routes/computer-use/types.d.ts +4 -41
- package/dist/routes/scripted/scenario.js +28 -12
- package/dist/routes/scripted/session-result.js +6 -3
- package/dist/routes/shared-world/bundle-records.js +2 -8
- package/dist/routes/shared-world/participant-specs.d.ts +0 -2
- package/dist/routes/shared-world/participant-specs.js +1 -20
- package/dist/routes/shared-world/plan.d.ts +3 -0
- package/dist/routes/shared-world/plan.js +23 -3
- package/dist/routes/shared-world/setup.js +2 -2
- package/dist/routes/terminal/ledger.d.ts +9 -9
- package/dist/routes/terminal/ledger.js +63 -40
- package/dist/routes/terminal/live-finish.js +7 -10
- package/dist/routes/terminal/plan.d.ts +3 -0
- package/dist/routes/terminal/plan.js +8 -5
- package/dist/routes/terminal/token-usage.d.ts +1 -2
- package/dist/routes/terminal/token-usage.js +4 -6
- package/dist/routes/terminal/types.d.ts +9 -1
- package/dist/run/bundle-shape.js +6 -1
- package/dist/run/bundle.d.ts +5 -3
- package/dist/run/contained-output.d.ts +53 -5
- package/dist/run/contained-output.js +141 -42
- package/dist/run/detail.d.ts +2 -1
- package/dist/run/detail.js +12 -7
- package/dist/run/escape-sequences.d.ts +11 -0
- package/dist/run/escape-sequences.js +93 -0
- package/dist/run/evidence-files.d.ts +8 -8
- package/dist/run/evidence-files.js +19 -23
- package/dist/run/locate.d.ts +32 -3
- package/dist/run/locate.js +45 -40
- package/dist/run/notes.d.ts +0 -8
- package/dist/run/notes.js +5 -51
- package/dist/run/outcomes.d.ts +24 -0
- package/dist/run/outcomes.js +19 -0
- package/dist/run/paths.d.ts +2 -0
- package/dist/run/paths.js +5 -1
- package/dist/run/preflight-receipts.js +13 -8
- package/dist/run/project.js +7 -4
- package/dist/run/reclaim-outcomes.d.ts +8 -6
- package/dist/run/reclaim-outcomes.js +10 -6
- package/dist/run/reclaim.d.ts +5 -0
- package/dist/run/reclaim.js +43 -16
- package/dist/run/run-clock.d.ts +50 -0
- package/dist/run/run-clock.js +66 -0
- package/dist/run/run.js +5 -2
- package/dist/run/sandbox-ids.d.ts +7 -2
- package/dist/run/sandbox-ids.js +27 -11
- package/dist/run/secrets.d.ts +2 -1
- package/dist/run/secrets.js +18 -22
- package/dist/run/stored-runs.js +14 -7
- package/dist/run/transient-comms-secrets.js +6 -17
- package/dist/run-study.js +22 -13
- package/dist/study/adapter-scorer-loader.js +6 -4
- package/dist/study/discover.js +9 -6
- package/dist/study/init-paths.js +7 -5
- package/dist/study/persona-resolve.d.ts +2 -2
- package/dist/study/persona-resolve.js +9 -5
- package/dist/study/plan-types.d.ts +2 -2
- package/dist/study/plan.d.ts +5 -3
- package/dist/study/plan.js +18 -9
- package/dist/study/preflight-probes.js +11 -3
- package/dist/study/preflight.js +9 -5
- package/dist/study/summary.d.ts +8 -2
- package/dist/study/summary.js +20 -22
- package/dist/study/warnings.d.ts +19 -12
- package/dist/study/warnings.js +38 -17
- package/dist/substrates/e2b/connection.d.ts +12 -0
- package/dist/substrates/e2b/connection.js +35 -0
- package/dist/substrates/e2b/desktop-executor.d.ts +1 -3
- package/dist/substrates/e2b/desktop-executor.js +42 -9
- package/dist/substrates/e2b/lifetime.d.ts +24 -4
- package/dist/substrates/e2b/lifetime.js +41 -4
- package/dist/substrates/e2b/sandbox.d.ts +4 -3
- package/dist/substrates/e2b/sandbox.js +13 -18
- package/dist/substrates/e2b/sdk.d.ts +20 -9
- package/dist/substrates/local/capacity.d.ts +18 -2
- package/dist/substrates/local/capacity.js +21 -2
- package/dist/substrates/local/runtime-host.js +21 -14
- package/dist/substrates/local/runtime.js +3 -11
- package/dist/tui/contract.d.ts +30 -5
- package/dist/tui/contract.js +4 -1
- package/dist/tui-app.js +149 -147
- package/dist/verify/artifacts.d.ts +9 -2
- package/dist/verify/artifacts.js +37 -13
- package/dist/verify/liveness.js +19 -5
- package/dist/verify/verify.js +54 -27
- package/docs/architecture/guest-desktop.md +7 -2
- package/docs/architecture/state-driven-executor.md +4 -2
- package/docs/contracts/run-bundle.md +18 -10
- package/docs/contracts/schemas.md +27 -18
- package/docs/contracts/study-analysis.md +10 -1
- package/docs/product/automatic-analysis.md +7 -1
- package/package.json +2 -2
|
@@ -2,11 +2,12 @@ import { constants } from "node:fs";
|
|
|
2
2
|
import { lstat, open, realpath } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { isPathInside, validatePreparedRunRootIdentity } from "./paths.js";
|
|
5
|
-
import { assertPreparedSelectedOutputDirectory, } from "./contained-output.js";
|
|
5
|
+
import { assertPreparedSelectedOutputDirectory, isSafeRelativeFilePath, readOpenedAtMost, } from "./contained-output.js";
|
|
6
6
|
import { isNodeError } from "./type-guards.js";
|
|
7
7
|
// Reading a file from a retained run directory for analysis: the path must be a plain relative path
|
|
8
8
|
// inside the run, the file a single-link regular file that stays inside it, and the read bounded.
|
|
9
|
-
// Evidence capture, analysis jobs, the analysis store, cost reading and export all read
|
|
9
|
+
// Evidence capture, analysis jobs, the analysis store, cost reading, verify and export all read
|
|
10
|
+
// through here.
|
|
10
11
|
/**
|
|
11
12
|
* Analysis inputs are retained local artifacts, never URLs or caller-selected outputs. The path is
|
|
12
13
|
* percent-decoded up to five times, and each round must still be a plain relative path, so an
|
|
@@ -87,29 +88,24 @@ export async function pathMissing(filePath) {
|
|
|
87
88
|
throw error;
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
|
-
/**
|
|
91
|
-
* At most `maxBytes` from the start of an opened file, or null when it holds more. It reads by
|
|
92
|
-
* position, in chunks of up to 64 KiB, and stops one byte past the limit, so a file that grew
|
|
93
|
-
* after it was opened is never read whole.
|
|
94
|
-
*/
|
|
95
|
-
export async function readOpenedAtMost(handle, maxBytes) {
|
|
96
|
-
const chunks = [];
|
|
97
|
-
let total = 0;
|
|
98
|
-
for (;;) {
|
|
99
|
-
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1 - total));
|
|
100
|
-
const { bytesRead } = await handle.read(chunk, 0, chunk.length, total);
|
|
101
|
-
if (bytesRead === 0)
|
|
102
|
-
return Buffer.concat(chunks, total);
|
|
103
|
-
total += bytesRead;
|
|
104
|
-
if (total > maxBytes)
|
|
105
|
-
return null;
|
|
106
|
-
chunks.push(chunk.subarray(0, bytesRead));
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
91
|
const unavailable = { state: "unavailable" };
|
|
110
|
-
/**
|
|
92
|
+
/** readUnchangedFile for an analysis input, whose path must also be an evidence path. */
|
|
111
93
|
export async function readBoundedFileResult(root, relativePath, maxBytes) {
|
|
112
|
-
|
|
94
|
+
return isEvidencePath(relativePath)
|
|
95
|
+
? readUnchangedFile(root, relativePath, maxBytes)
|
|
96
|
+
: unavailable;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* At most `maxBytes` of a contained single-link regular file that stays the same file, with the
|
|
100
|
+
* same size and times, from the first check to the last. Size refusals are distinguished only after
|
|
101
|
+
* the same checks. `relativePath` uses `/`, and any name a contained read accepts
|
|
102
|
+
* (isSafeRelativeFilePath) is read: verify and export read the names they always did.
|
|
103
|
+
*/
|
|
104
|
+
export async function readUnchangedFile(root, relativePath, maxBytes) {
|
|
105
|
+
if (!isSafeRelativeFilePath(relativePath) ||
|
|
106
|
+
relativePath.includes("\\") ||
|
|
107
|
+
!Number.isSafeInteger(maxBytes) ||
|
|
108
|
+
maxBytes < 1)
|
|
113
109
|
return unavailable;
|
|
114
110
|
const validateRoot = async () => {
|
|
115
111
|
if ("physicalRunRoot" in root) {
|
package/dist/run/locate.d.ts
CHANGED
|
@@ -1,14 +1,43 @@
|
|
|
1
1
|
import { type PreparedRunArtifactPaths } from "./paths.js";
|
|
2
|
-
import { type PreparedSelectedOutputDirectory } from "./contained-output.js";
|
|
2
|
+
import { type ContainedRefusal, type PreparedSelectedOutputDirectory } from "./contained-output.js";
|
|
3
3
|
import type { RunPointer } from "./results.js";
|
|
4
4
|
import { type RunBundle } from "./bundle.js";
|
|
5
|
+
import { type BoundedFileResult } from "./evidence-files.js";
|
|
5
6
|
/** The run input that resolves through latest.json rather than naming a run directory. */
|
|
6
7
|
export declare const LATEST_RUN_ALIAS = "latest";
|
|
7
8
|
/** Resolve "latest" or an explicit run id to its prepared artifact paths. */
|
|
8
9
|
export declare function resolveRunPath(cwd: string, runInput: string): Promise<PreparedRunArtifactPaths | null>;
|
|
9
10
|
export declare function readLatest(runsRoot: PreparedSelectedOutputDirectory): Promise<RunPointer | null>;
|
|
10
|
-
|
|
11
|
-
export
|
|
11
|
+
/** A run file as text: its text, nothing at the path, or why it was refused. */
|
|
12
|
+
export type RunTextRead = {
|
|
13
|
+
status: "read";
|
|
14
|
+
text: string;
|
|
15
|
+
} | {
|
|
16
|
+
status: "missing";
|
|
17
|
+
} | ContainedRefusal;
|
|
18
|
+
/** A run file as JSON. `value` is undefined when the file is there but is not valid JSON. */
|
|
19
|
+
export type RunJsonRead = {
|
|
20
|
+
status: "read";
|
|
21
|
+
value: unknown;
|
|
22
|
+
} | {
|
|
23
|
+
status: "missing";
|
|
24
|
+
} | ContainedRefusal;
|
|
25
|
+
/**
|
|
26
|
+
* The JSON, or null for a file that is missing, refused or not valid JSON: for a caller that
|
|
27
|
+
* treats the three alike, as every reader did before a refusal had its own state.
|
|
28
|
+
*/
|
|
29
|
+
export declare function runJsonValue(read: RunJsonRead): unknown;
|
|
30
|
+
/** A run file as JSON, within RUN_ARTIFACT_MAX_BYTES. */
|
|
31
|
+
export declare function readRunJsonIfExists(runPaths: PreparedRunArtifactPaths, ...segments: string[]): Promise<RunJsonRead>;
|
|
32
|
+
/** A run file as text, within RUN_ARTIFACT_MAX_BYTES. */
|
|
33
|
+
export declare function readRunTextIfExists(runPaths: PreparedRunArtifactPaths, ...segments: string[]): Promise<RunTextRead>;
|
|
34
|
+
/**
|
|
35
|
+
* A run file, read whole through readUnchangedFile: `limit` when it holds more than
|
|
36
|
+
* RUN_ARTIFACT_MAX_BYTES, `unavailable` when it is missing, unsafe, or changes while it is read.
|
|
37
|
+
* A `\` in the path reads as `/`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readSafeRunArtifact(runPaths: PreparedRunArtifactPaths, relativePath: string): Promise<BoundedFileResult>;
|
|
40
|
+
/** readSafeRunArtifact's bytes, or null for any file it does not read. */
|
|
12
41
|
export declare function readSafeRunArtifactBytes(runPaths: PreparedRunArtifactPaths, relativePath: string): Promise<Buffer | null>;
|
|
13
42
|
export declare function readSafeRunArtifactJson(runPaths: PreparedRunArtifactPaths, relativePath: string): Promise<unknown>;
|
|
14
43
|
export declare function loadRunBundle(cwdInput: string, runInput: string): Promise<{
|
package/dist/run/locate.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { lstat } from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
3
|
-
import { bindExistingRunArtifactPaths, isSafeRunIdSegment, resolveExistingRunDirectory, resolveLatestRunDirectory, validatePreparedRunArtifactPaths, } from "./paths.js";
|
|
4
|
-
import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, readContainedRegularFile, } from "./contained-output.js";
|
|
2
|
+
import { bindExistingRunArtifactPaths, isSafeRunIdSegment, resolveExistingRunDirectory, resolveLatestRunDirectory, RUNS_RELATIVE_ROOT, validatePreparedRunArtifactPaths, } from "./paths.js";
|
|
3
|
+
import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, ContainedReadRefusedError, LATEST_POINTER_MAX_BYTES, readContainedRegularFile, RUN_ARTIFACT_MAX_BYTES, } from "./contained-output.js";
|
|
5
4
|
import { RUN_BUNDLE_FILE } from "./bundle.js";
|
|
6
5
|
import { isRunBundle, isRunPointer } from "./bundle-shape.js";
|
|
7
|
-
import {
|
|
6
|
+
import { readUnchangedFile } from "./evidence-files.js";
|
|
8
7
|
/** The run input that resolves through latest.json rather than naming a run directory. */
|
|
9
8
|
export const LATEST_RUN_ALIAS = "latest";
|
|
10
9
|
/** Resolve "latest" or an explicit run id to its prepared artifact paths. */
|
|
@@ -33,59 +32,61 @@ export async function resolveRunPath(cwd, runInput) {
|
|
|
33
32
|
return bindExistingRunArtifactPaths(cwd, runInput);
|
|
34
33
|
}
|
|
35
34
|
export async function readLatest(runsRoot) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
throw error;
|
|
46
|
-
}
|
|
47
|
-
if (latestStats.isSymbolicLink() || !latestStats.isFile() || latestStats.nlink !== 1n) {
|
|
48
|
-
throw new Error("Latest run pointer must be a single-link regular file.");
|
|
49
|
-
}
|
|
50
|
-
const bytes = await readContainedRegularFile(runsRoot, "latest.json");
|
|
51
|
-
if (!bytes) {
|
|
52
|
-
throw new Error("Latest run pointer changed while it was being read.");
|
|
35
|
+
const read = await readContainedRegularFile(runsRoot, "latest.json", LATEST_POINTER_MAX_BYTES);
|
|
36
|
+
if (read.status === "missing")
|
|
37
|
+
return null;
|
|
38
|
+
if (read.status === "refused") {
|
|
39
|
+
if (read.reason === "too-large")
|
|
40
|
+
throw new ContainedReadRefusedError(path.join(RUNS_RELATIVE_ROOT, "latest.json"), read);
|
|
41
|
+
throw new Error(read.reason === "not-regular" || read.reason === "directory"
|
|
42
|
+
? "Latest run pointer must be a single-link regular file."
|
|
43
|
+
: "Latest run pointer changed while it was being read.");
|
|
53
44
|
}
|
|
54
45
|
let latest;
|
|
55
46
|
try {
|
|
56
|
-
latest = JSON.parse(bytes.toString("utf8"));
|
|
47
|
+
latest = JSON.parse(read.bytes.toString("utf8"));
|
|
57
48
|
}
|
|
58
49
|
catch {
|
|
59
50
|
return null;
|
|
60
51
|
}
|
|
61
52
|
return isRunPointer(latest) ? latest : null;
|
|
62
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The JSON, or null for a file that is missing, refused or not valid JSON: for a caller that
|
|
56
|
+
* treats the three alike, as every reader did before a refusal had its own state.
|
|
57
|
+
*/
|
|
58
|
+
export function runJsonValue(read) {
|
|
59
|
+
return read.status === "read" ? (read.value ?? null) : null;
|
|
60
|
+
}
|
|
61
|
+
/** A run file as JSON, within RUN_ARTIFACT_MAX_BYTES. */
|
|
63
62
|
export async function readRunJsonIfExists(runPaths, ...segments) {
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
return
|
|
67
|
-
}
|
|
63
|
+
const read = await readRunTextIfExists(runPaths, ...segments);
|
|
64
|
+
if (read.status !== "read")
|
|
65
|
+
return read;
|
|
68
66
|
try {
|
|
69
|
-
return JSON.parse(text);
|
|
67
|
+
return { status: "read", value: JSON.parse(read.text) };
|
|
70
68
|
}
|
|
71
69
|
catch {
|
|
72
|
-
return
|
|
70
|
+
return { status: "read", value: undefined };
|
|
73
71
|
}
|
|
74
72
|
}
|
|
73
|
+
/** A run file as text, within RUN_ARTIFACT_MAX_BYTES. */
|
|
75
74
|
export async function readRunTextIfExists(runPaths, ...segments) {
|
|
76
|
-
const
|
|
77
|
-
return bytes
|
|
75
|
+
const read = await readContainedRegularFile(runPaths, segments.join("/"), RUN_ARTIFACT_MAX_BYTES);
|
|
76
|
+
return read.status === "read" ? { status: "read", text: read.bytes.toString("utf8") } : read;
|
|
78
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* A run file, read whole through readUnchangedFile: `limit` when it holds more than
|
|
80
|
+
* RUN_ARTIFACT_MAX_BYTES, `unavailable` when it is missing, unsafe, or changes while it is read.
|
|
81
|
+
* A `\` in the path reads as `/`.
|
|
82
|
+
*/
|
|
83
|
+
export async function readSafeRunArtifact(runPaths, relativePath) {
|
|
84
|
+
return readUnchangedFile(runPaths, relativePath.replace(/\\/g, "/"), RUN_ARTIFACT_MAX_BYTES);
|
|
85
|
+
}
|
|
86
|
+
/** readSafeRunArtifact's bytes, or null for any file it does not read. */
|
|
79
87
|
export async function readSafeRunArtifactBytes(runPaths, relativePath) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
if (path.isAbsolute(relativePath) ||
|
|
83
|
-
path.win32.isAbsolute(relativePath) ||
|
|
84
|
-
segments.length === 0 ||
|
|
85
|
-
segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
return readContainedRegularFile(runPaths, normalized);
|
|
88
|
+
const read = await readSafeRunArtifact(runPaths, relativePath);
|
|
89
|
+
return read.state === "read" ? read.bytes : null;
|
|
89
90
|
}
|
|
90
91
|
export async function readSafeRunArtifactJson(runPaths, relativePath) {
|
|
91
92
|
const bytes = await readSafeRunArtifactBytes(runPaths, relativePath);
|
|
@@ -112,7 +113,11 @@ export async function loadRunBundlePrepared(cwdInput, runPaths) {
|
|
|
112
113
|
const cwd = path.resolve(cwdInput);
|
|
113
114
|
await validatePreparedRunArtifactPaths(runPaths);
|
|
114
115
|
const bundlePath = path.join(runPaths.absoluteRunRoot, RUN_BUNDLE_FILE);
|
|
115
|
-
const
|
|
116
|
+
const read = await readRunJsonIfExists(runPaths, RUN_BUNDLE_FILE);
|
|
117
|
+
// A run.json it refuses is no reason to call the run absent: the caller reports why.
|
|
118
|
+
if (read.status === "refused")
|
|
119
|
+
throw new ContainedReadRefusedError(RUN_BUNDLE_FILE, read);
|
|
120
|
+
const bundle = read.status === "read" ? read.value : null;
|
|
116
121
|
if (!isRunBundle(bundle)) {
|
|
117
122
|
return null;
|
|
118
123
|
}
|
package/dist/run/notes.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { RunBundle } from "./bundle.js";
|
|
2
1
|
import { type RunNote } from "./note-shape.js";
|
|
3
2
|
import { type PreparedRunArtifactPaths } from "./paths.js";
|
|
4
3
|
export interface RunNoteInput {
|
|
@@ -20,13 +19,6 @@ export type AddRunNoteResult =
|
|
|
20
19
|
message: string;
|
|
21
20
|
};
|
|
22
21
|
};
|
|
23
|
-
/** A run clock time as the Observer shows it: whole minutes and seconds, `02:31`. */
|
|
24
|
-
export declare function formatRunTime(ms: number): string;
|
|
25
|
-
/** The run clock: the first and last timed capture or desktop video moment, in epoch ms. */
|
|
26
|
-
export declare function runClock(bundle: RunBundle): {
|
|
27
|
-
startMs: number;
|
|
28
|
-
endMs: number;
|
|
29
|
-
} | null;
|
|
30
22
|
/**
|
|
31
23
|
* Adds a note as a new file, notes/<id>.json, created under a name nothing holds. It reads no other
|
|
32
24
|
* note and replaces nothing, so notes added at the same time, from any process, are all kept.
|
package/dist/run/notes.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Reviewer notes: free text a person adds at a moment of a recorded run. Each note is its own file
|
|
2
|
-
// in the run directory (note-files.ts). A note's time counts from the run clock
|
|
3
|
-
//
|
|
2
|
+
// in the run directory (note-files.ts). A note's time counts from the start of the run clock
|
|
3
|
+
// (run-clock.ts), which is the Observer's study timeline.
|
|
4
4
|
import { cli } from "../cli/invocation.js";
|
|
5
5
|
import { redactText } from "../evidence/redaction.js";
|
|
6
6
|
import { createContainedOutputFile } from "./contained-output.js";
|
|
@@ -9,6 +9,7 @@ import { countRunNotes, encodeRunNote, newRunNoteId, runNoteFile } from "./note-
|
|
|
9
9
|
import { MAX_NOTE_TEXT, MAX_RUN_NOTES } from "./note-shape.js";
|
|
10
10
|
import { runParticipantCaptions, streamParticipantIdOf } from "./participant-records.js";
|
|
11
11
|
import { physicalCwdOf, runIdOf } from "./paths.js";
|
|
12
|
+
import { formatRunTime, runClock, traceItems } from "./run-clock.js";
|
|
12
13
|
import { transientCommsKnownValueScrub } from "./transient-comms-secrets.js";
|
|
13
14
|
import { isNodeError } from "./type-guards.js";
|
|
14
15
|
const DEFAULT_AUTHOR = "you";
|
|
@@ -38,59 +39,12 @@ function participantStreamId(bundle, named) {
|
|
|
38
39
|
const known = bundle.streams.map((stream) => `${captions.get(stream.id)} (${streamParticipantIdOf(stream) ?? stream.id})`);
|
|
39
40
|
return refuse("HUMANISH_NOTE_UNKNOWN_PARTICIPANT", `Run ${bundle.runId} has no participant ${named}. Its participants are ${known.join(", ")}; leave the participant out for a note on the whole run.`);
|
|
40
41
|
}
|
|
41
|
-
/** A run clock time as the Observer shows it: whole minutes and seconds, `02:31`. */
|
|
42
|
-
export function formatRunTime(ms) {
|
|
43
|
-
const seconds = Math.max(0, Math.floor(ms / 1000));
|
|
44
|
-
return `${String(Math.floor(seconds / 60)).padStart(2, "0")}:${String(seconds % 60).padStart(2, "0")}`;
|
|
45
|
-
}
|
|
46
|
-
/** The run clock: the first and last timed capture or desktop video moment, in epoch ms. */
|
|
47
|
-
export function runClock(bundle) {
|
|
48
|
-
const moments = bundle.streams.flatMap((stream) => [
|
|
49
|
-
...(captureTimes(stream) ?? []),
|
|
50
|
-
...recordingMoments(stream),
|
|
51
|
-
]);
|
|
52
|
-
if (moments.length === 0)
|
|
53
|
-
return null;
|
|
54
|
-
// A loop, since spreading a long recording's capture times into Math.min can overflow the stack.
|
|
55
|
-
let startMs = Infinity;
|
|
56
|
-
let endMs = -Infinity;
|
|
57
|
-
for (const moment of moments) {
|
|
58
|
-
startMs = Math.min(startMs, moment);
|
|
59
|
-
endMs = Math.max(endMs, moment);
|
|
60
|
-
}
|
|
61
|
-
return { startMs, endMs };
|
|
62
|
-
}
|
|
63
|
-
function traceItems(stream) {
|
|
64
|
-
return stream.actor?.items ?? stream.liveActor?.items ?? [];
|
|
65
|
-
}
|
|
66
|
-
const stamp = (item) => item.at === undefined ? Number.NaN : Date.parse(item.at);
|
|
67
|
-
/**
|
|
68
|
-
* A participant's capture times, as the Observer's study clock reads them: only when every capture
|
|
69
|
-
* is stamped and the stamps never go back. Otherwise the participant adds nothing to the clock.
|
|
70
|
-
*/
|
|
71
|
-
function captureTimes(stream) {
|
|
72
|
-
const times = traceItems(stream)
|
|
73
|
-
.filter((item) => (item.kind === "screenshot" || item.kind === "ui_action") && item.screenshotRef)
|
|
74
|
-
.map(stamp);
|
|
75
|
-
const ordered = times.every((time, index) => Number.isFinite(time) && (index === 0 || time >= times[index - 1]));
|
|
76
|
-
return ordered ? times : null;
|
|
77
|
-
}
|
|
78
|
-
function recordingMoments(stream) {
|
|
79
|
-
const recording = stream.recording;
|
|
80
|
-
if (!recording)
|
|
81
|
-
return [];
|
|
82
|
-
const startMs = Date.parse(recording.startedAt);
|
|
83
|
-
const endMs = startMs + recording.durationMs;
|
|
84
|
-
return Number.isFinite(startMs) && Number.isFinite(endMs) && recording.durationMs > 0
|
|
85
|
-
? [startMs, endMs]
|
|
86
|
-
: [];
|
|
87
|
-
}
|
|
88
42
|
/** The latest stamped trace item at or before `moment`, among the given participants. */
|
|
89
43
|
function nearestItem(streams, moment) {
|
|
90
44
|
let best = null;
|
|
91
45
|
for (const stream of streams)
|
|
92
46
|
for (const item of traceItems(stream)) {
|
|
93
|
-
const time =
|
|
47
|
+
const time = item.at === undefined ? Number.NaN : Date.parse(item.at);
|
|
94
48
|
if (Number.isFinite(time) && time <= moment && (best === null || time >= best.time))
|
|
95
49
|
best = { participant: stream.id, itemId: item.id, time };
|
|
96
50
|
}
|
|
@@ -143,7 +97,7 @@ export async function addRunNote(prepared, input) {
|
|
|
143
97
|
if (!loaded)
|
|
144
98
|
return refuse("HUMANISH_INVALID_RUN_BUNDLE", `Run ${runIdOf(prepared)} has no run.json humanish can read safely, so no note was added. \`${cli(`verify --run ${runIdOf(prepared)}`)}\` says what is wrong with it.`);
|
|
145
99
|
const { bundle } = loaded;
|
|
146
|
-
const clock = runClock(bundle);
|
|
100
|
+
const clock = runClock(bundle.streams);
|
|
147
101
|
if (!clock)
|
|
148
102
|
return refuse("HUMANISH_NOTE_NO_CLOCK", `Run ${bundle.runId} has no captures with recorded times, so a note cannot point at a moment of it.`);
|
|
149
103
|
const participant = input.participant === null ? null : participantStreamId(bundle, input.participant);
|
package/dist/run/outcomes.d.ts
CHANGED
|
@@ -21,6 +21,30 @@ type ParticipantOutcomeDetail = {
|
|
|
21
21
|
* goal says where the claim came from, as the run's participant line does.
|
|
22
22
|
*/
|
|
23
23
|
export declare function sessionEndingInWords(trace: ActorTrace): string;
|
|
24
|
+
/** The app a participant summary says the participant used. */
|
|
25
|
+
interface SummaryApp {
|
|
26
|
+
/** "the app", or "the shared app" when every participant uses one instance. */
|
|
27
|
+
name: string;
|
|
28
|
+
/** Where the participant's browser ran, such as "in a hosted desktop browser". */
|
|
29
|
+
place?: string;
|
|
30
|
+
/** What a participant that never ran would have used, such as the app's URL. Defaults to `name`. */
|
|
31
|
+
target?: string;
|
|
32
|
+
}
|
|
33
|
+
/** What one participant's session came to when the bundle is written. */
|
|
34
|
+
interface SummarySession {
|
|
35
|
+
trace?: ActorTrace | undefined;
|
|
36
|
+
/** Why the run skipped the participant before its session started. */
|
|
37
|
+
skippedReason?: string | undefined;
|
|
38
|
+
sessionError?: string | undefined;
|
|
39
|
+
/** The run is still going and has no outcome for this participant. */
|
|
40
|
+
inProgress: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The sentence a participant's simulation record carries. An ended session, a skip and a session
|
|
44
|
+
* error each say what happened, in that order; a participant without one is still using the app or
|
|
45
|
+
* never ran.
|
|
46
|
+
*/
|
|
47
|
+
export declare function participantSummary(name: string, app: SummaryApp, session: SummarySession): string;
|
|
24
48
|
/** "; the gaps list the other 2", or "all 3" when nobody passed. Empty when the gaps list nobody. */
|
|
25
49
|
export declare function gapsListClause(listed: number, total: number): string;
|
|
26
50
|
/** One line a stakeholder can read, with the denominator attached to every number. */
|
package/dist/run/outcomes.js
CHANGED
|
@@ -146,6 +146,25 @@ export function sessionEndingInWords(trace) {
|
|
|
146
146
|
return "ended";
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* The sentence a participant's simulation record carries. An ended session, a skip and a session
|
|
151
|
+
* error each say what happened, in that order; a participant without one is still using the app or
|
|
152
|
+
* never ran.
|
|
153
|
+
*/
|
|
154
|
+
export function participantSummary(name, app, session) {
|
|
155
|
+
const where = app.place === undefined ? app.name : `${app.name} ${app.place}`;
|
|
156
|
+
if (session.trace !== undefined) {
|
|
157
|
+
const used = app.place === undefined ? where : `${where},`;
|
|
158
|
+
return `${name} used ${used} and ${sessionEndingInWords(session.trace)}.`;
|
|
159
|
+
}
|
|
160
|
+
if (session.skippedReason !== undefined)
|
|
161
|
+
return `${name} was ${session.skippedReason}.`;
|
|
162
|
+
if (session.sessionError !== undefined)
|
|
163
|
+
return `${name} did not finish a session: ${session.sessionError}`;
|
|
164
|
+
if (session.inProgress)
|
|
165
|
+
return `${name} is using ${where}.`;
|
|
166
|
+
return `${name} would use ${app.target ?? app.name}; no session ran.`;
|
|
167
|
+
}
|
|
149
168
|
/** "; the gaps list the other 2", or "all 3" when nobody passed. Empty when the gaps list nobody. */
|
|
150
169
|
export function gapsListClause(listed, total) {
|
|
151
170
|
if (listed <= 0)
|
package/dist/run/paths.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface LatestRunPathPointer {
|
|
|
27
27
|
*/
|
|
28
28
|
export declare function isSafeRunIdSegment(runId: string): boolean;
|
|
29
29
|
export declare function resolveRunsRoot(cwdInput: string): string;
|
|
30
|
+
/** The latest-run pointer, `.humanish/runs/latest.json`, which the newest run writes. */
|
|
31
|
+
export declare function resolveLatestRunPointer(cwdInput: string): string;
|
|
30
32
|
export declare function prepareRunArtifactPaths(cwdInput: string, runId: string): Promise<PreparedRunArtifactPaths>;
|
|
31
33
|
/** A new run's id names a directory that already exists. */
|
|
32
34
|
export interface RunIdInUse {
|
package/dist/run/paths.js
CHANGED
|
@@ -18,6 +18,10 @@ export function isSafeRunIdSegment(runId) {
|
|
|
18
18
|
export function resolveRunsRoot(cwdInput) {
|
|
19
19
|
return path.resolve(cwdInput, RUNS_RELATIVE_ROOT);
|
|
20
20
|
}
|
|
21
|
+
/** The latest-run pointer, `.humanish/runs/latest.json`, which the newest run writes. */
|
|
22
|
+
export function resolveLatestRunPointer(cwdInput) {
|
|
23
|
+
return path.resolve(cwdInput, LATEST_RUN_RELATIVE_PATH);
|
|
24
|
+
}
|
|
21
25
|
function resolveRunDirectory(cwdInput, runId) {
|
|
22
26
|
if (!isSafeRunIdSegment(runId)) {
|
|
23
27
|
throw new Error("Run id must be one non-empty path segment.");
|
|
@@ -45,7 +49,7 @@ function resolveRunArtifactPaths(cwdInput, runId) {
|
|
|
45
49
|
return {
|
|
46
50
|
absoluteRunRoot,
|
|
47
51
|
relativeRunRoot: path.join(RUNS_RELATIVE_ROOT, runId),
|
|
48
|
-
absoluteLatestPointer:
|
|
52
|
+
absoluteLatestPointer: resolveLatestRunPointer(cwdInput),
|
|
49
53
|
relativeLatestPointer: LATEST_RUN_RELATIVE_PATH,
|
|
50
54
|
};
|
|
51
55
|
}
|
|
@@ -8,7 +8,7 @@ import { readdir, rmdir, unlink } from "node:fs/promises";
|
|
|
8
8
|
import { hostname } from "node:os";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { parseSandboxReceipts, SANDBOX_RECEIPTS_ARTIFACT } from "./sandbox-receipts.js";
|
|
11
|
-
import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, prepareManagedHumanishOutputDirectory, readContainedRegularFile, writeContainedOutputFile, } from "./contained-output.js";
|
|
11
|
+
import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, prepareManagedHumanishOutputDirectory, readContainedRegularFile, RUN_ARTIFACT_MAX_BYTES, writeContainedOutputFile, } from "./contained-output.js";
|
|
12
12
|
const PREFLIGHT_DIR = "preflight";
|
|
13
13
|
// Written when the probe finished without confirming its kill: the owner no longer holds the
|
|
14
14
|
// sandbox, so reclaim may act even while the owning process lives on.
|
|
@@ -54,11 +54,12 @@ export async function openPreflightJournal(cwd, leaseMs) {
|
|
|
54
54
|
await writeContainedOutputFile(root, OWNER_FILE, `${JSON.stringify(owner)}\n`, "utf8");
|
|
55
55
|
return { id, root, owner };
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
/** The owner record, or undefined when it is missing, refused or malformed: "missing or unreadable". */
|
|
58
|
+
function parseOwner(read) {
|
|
59
|
+
if (read.status !== "read")
|
|
59
60
|
return undefined;
|
|
60
61
|
try {
|
|
61
|
-
const value = JSON.parse(bytes.toString("utf8"));
|
|
62
|
+
const value = JSON.parse(read.bytes.toString("utf8"));
|
|
62
63
|
if (typeof value.hostname !== "string" ||
|
|
63
64
|
typeof value.pid !== "number" ||
|
|
64
65
|
typeof value.createdAt !== "string" ||
|
|
@@ -90,7 +91,7 @@ export async function listPreflightJournals(cwd) {
|
|
|
90
91
|
const root = await bindExistingManagedHumanishOutputDirectory(cwd, PREFLIGHT_DIR, entry.name);
|
|
91
92
|
if (!root)
|
|
92
93
|
continue;
|
|
93
|
-
const owner = parseOwner(await readContainedRegularFile(root, OWNER_FILE));
|
|
94
|
+
const owner = parseOwner(await readContainedRegularFile(root, OWNER_FILE, RUN_ARTIFACT_MAX_BYTES));
|
|
94
95
|
journals.push({ id: entry.name, root, ...(owner === undefined ? {} : { owner }) });
|
|
95
96
|
}
|
|
96
97
|
return journals;
|
|
@@ -118,10 +119,14 @@ function leaseEndsAt(journal, receipts) {
|
|
|
118
119
|
* another process). Anything else may be a live probe, and the reason says why it was left.
|
|
119
120
|
*/
|
|
120
121
|
export async function preflightReclaimDecision(journal, nowMs) {
|
|
121
|
-
|
|
122
|
+
// Only a marker that reads hands the journal over: one refused (a link, say) keeps it.
|
|
123
|
+
const marker = await readContainedRegularFile(journal.root, ABANDONED_MARKER, RUN_ARTIFACT_MAX_BYTES);
|
|
124
|
+
if (marker.status === "read")
|
|
122
125
|
return { reclaim: true };
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
// Refused receipts give no lease, as missing ones do; the owner's lease still decides, and
|
|
127
|
+
// reclaim refuses the unreadable journal before it kills anything.
|
|
128
|
+
const receipts = await readContainedRegularFile(journal.root, SANDBOX_RECEIPTS_ARTIFACT, RUN_ARTIFACT_MAX_BYTES);
|
|
129
|
+
const endsAt = leaseEndsAt(journal, receipts.status === "read" ? receipts.bytes.toString("utf8") : null);
|
|
125
130
|
if (endsAt !== undefined && nowMs > endsAt)
|
|
126
131
|
return { reclaim: true };
|
|
127
132
|
const until = endsAt === undefined ? "" : `; its lease ends at ${new Date(endsAt).toISOString()}`;
|
package/dist/run/project.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// (package.json, humanish/ personas and scenarios) without following links out of it.
|
|
3
3
|
import { lstat, stat } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, readContainedRegularFile, } from "./contained-output.js";
|
|
5
|
+
import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, readContainedRegularFile, refusalText, PROJECT_FILE_MAX_BYTES, } from "./contained-output.js";
|
|
6
6
|
import { isNodeError, isRecord } from "./type-guards.js";
|
|
7
7
|
async function inspectImplicitProjectPath(projectRoot, relativePath) {
|
|
8
8
|
const segments = relativePath.replace(/\\/g, "/").split("/");
|
|
@@ -61,11 +61,14 @@ export async function readImplicitProjectFile(projectRoot, relativePath) {
|
|
|
61
61
|
if (!stats.isFile() || stats.nlink !== 1n) {
|
|
62
62
|
throw new Error(`Implicit project file must be a single-link regular file: ${relativePath}`);
|
|
63
63
|
}
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
64
|
+
const read = await readContainedRegularFile(projectRoot, relativePath.replace(/\\/g, "/"), PROJECT_FILE_MAX_BYTES);
|
|
65
|
+
if (read.status === "refused" && read.reason === "too-large")
|
|
66
|
+
throw new Error(`Implicit project file ${refusalText(relativePath, read)}.`);
|
|
67
|
+
// Gone or swapped since it was inspected above.
|
|
68
|
+
if (read.status !== "read") {
|
|
66
69
|
throw new Error(`Implicit project file changed while it was being read: ${relativePath}`);
|
|
67
70
|
}
|
|
68
|
-
return bytes.toString("utf8");
|
|
71
|
+
return read.bytes.toString("utf8");
|
|
69
72
|
}
|
|
70
73
|
export async function readPackageName(projectRoot) {
|
|
71
74
|
const text = await readImplicitProjectFile(projectRoot, "package.json");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type E2BTagSearch } from "../substrates/e2b/sandbox.js";
|
|
2
|
-
import type {
|
|
3
|
-
import { type PreparedOutputRoot } from "./contained-output.js";
|
|
2
|
+
import type { E2BAccount } from "../substrates/e2b/connection.js";
|
|
3
|
+
import { type ContainedRefusal, type PreparedOutputRoot } from "./contained-output.js";
|
|
4
4
|
export declare const RECLAIM_RESULT_SCHEMA = "humanish.reclaim-result.v1";
|
|
5
5
|
export declare const RECLAIM_RECEIPT_ARTIFACT = "reclaim-receipt.json";
|
|
6
6
|
/**
|
|
@@ -50,7 +50,7 @@ export interface Target {
|
|
|
50
50
|
* kills leaves a record of each one not yet confirmed gone.
|
|
51
51
|
*/
|
|
52
52
|
export declare function targetSet(options: {
|
|
53
|
-
e2b:
|
|
53
|
+
e2b: E2BAccount | undefined;
|
|
54
54
|
check: boolean;
|
|
55
55
|
requestTimeoutMs: number;
|
|
56
56
|
earlier: ReclaimOutcome[];
|
|
@@ -66,16 +66,18 @@ export declare function targetSet(options: {
|
|
|
66
66
|
finish(outcomes: readonly ReclaimOutcome[], state: ReclaimState): Promise<void>;
|
|
67
67
|
};
|
|
68
68
|
/** Search E2B by each distinct tag set; done only when every search finished. */
|
|
69
|
-
export declare function searchTagSets(e2b:
|
|
69
|
+
export declare function searchTagSets(e2b: E2BAccount | undefined, tagSets: readonly Record<string, string>[], options: {
|
|
70
70
|
requestTimeoutMs: number;
|
|
71
71
|
unavailable: string;
|
|
72
72
|
}): Promise<E2BTagSearch>;
|
|
73
73
|
export declare const GONE: Set<ReclaimOutcomeState>;
|
|
74
74
|
/**
|
|
75
75
|
* The outcomes an earlier reclaim of this directory recorded, by digest. A receipt written before
|
|
76
|
-
* 0.110 names raw ids, which are digested;
|
|
76
|
+
* 0.110 names raw ids, which are digested; a missing, malformed or foreign receipt carries
|
|
77
|
+
* nothing. A receipt that is there and refused is returned as the refusal, so reclaim stops
|
|
78
|
+
* before its own receipt replaces one it could not read.
|
|
77
79
|
*/
|
|
78
|
-
export declare function earlierOutcomes(root: PreparedOutputRoot, label: string): Promise<ReclaimOutcome[]>;
|
|
80
|
+
export declare function earlierOutcomes(root: PreparedOutputRoot, label: string): Promise<ReclaimOutcome[] | ContainedRefusal>;
|
|
79
81
|
export declare function rootState(outcomes: readonly ReclaimOutcome[], check: boolean, searched: boolean): ReclaimState;
|
|
80
82
|
/** Resolves when `settled` does, or after `ms` if given, whichever is first. */
|
|
81
83
|
export declare function settledWithin(settled: Promise<void>, ms: number | undefined): Promise<void>;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// receipt writer. src/run/reclaim.ts decides what to find; this module records what happened.
|
|
5
5
|
import { REDACTED_SANDBOX_ID, redactText, sandboxIdDigest, toErrorMessage, } from "../evidence/redaction.js";
|
|
6
6
|
import { destroyE2BSandbox, findE2BSandboxesByTags, inspectE2BSandbox, } from "../substrates/e2b/sandbox.js";
|
|
7
|
-
import { readContainedRegularFile, writeContainedOutputFile, } from "./contained-output.js";
|
|
7
|
+
import { readContainedRegularFile, RUN_ARTIFACT_MAX_BYTES, writeContainedOutputFile, } from "./contained-output.js";
|
|
8
8
|
import { scrubSandboxIds } from "./sandbox-ids.js";
|
|
9
9
|
import { isRecord } from "./type-guards.js";
|
|
10
10
|
export const RECLAIM_RESULT_SCHEMA = "humanish.reclaim-result.v1";
|
|
@@ -122,15 +122,19 @@ const OUTCOME_STATES = new Set([
|
|
|
122
122
|
]);
|
|
123
123
|
/**
|
|
124
124
|
* The outcomes an earlier reclaim of this directory recorded, by digest. A receipt written before
|
|
125
|
-
* 0.110 names raw ids, which are digested;
|
|
125
|
+
* 0.110 names raw ids, which are digested; a missing, malformed or foreign receipt carries
|
|
126
|
+
* nothing. A receipt that is there and refused is returned as the refusal, so reclaim stops
|
|
127
|
+
* before its own receipt replaces one it could not read.
|
|
126
128
|
*/
|
|
127
129
|
export async function earlierOutcomes(root, label) {
|
|
130
|
+
const read = await readContainedRegularFile(root, RECLAIM_RECEIPT_ARTIFACT, RUN_ARTIFACT_MAX_BYTES);
|
|
131
|
+
if (read.status === "refused")
|
|
132
|
+
return read;
|
|
133
|
+
if (read.status === "missing")
|
|
134
|
+
return [];
|
|
128
135
|
let parsed;
|
|
129
136
|
try {
|
|
130
|
-
|
|
131
|
-
if (bytes === null)
|
|
132
|
-
return [];
|
|
133
|
-
parsed = JSON.parse(bytes.toString("utf8"));
|
|
137
|
+
parsed = JSON.parse(read.bytes.toString("utf8"));
|
|
134
138
|
}
|
|
135
139
|
catch {
|
|
136
140
|
return [];
|
package/dist/run/reclaim.d.ts
CHANGED
|
@@ -33,6 +33,11 @@ export interface ReclaimResult {
|
|
|
33
33
|
export interface ReclaimHooks {
|
|
34
34
|
/** Tests inject a fake @e2b/desktop module; default loads the real one. */
|
|
35
35
|
loadModule?: () => Promise<E2BDesktopModule>;
|
|
36
|
+
/**
|
|
37
|
+
* The env whose E2B_API_KEY and E2B_DOMAIN every E2B call carries. Defaults to process.env, where
|
|
38
|
+
* the CLI put the run's key before the run started.
|
|
39
|
+
*/
|
|
40
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
36
41
|
requestTimeoutMs?: number;
|
|
37
42
|
/** Ask E2B whether each sandbox still exists, and kill nothing. */
|
|
38
43
|
check?: boolean;
|