humanish 0.110.0 → 0.110.2
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/ARCHITECTURE.md +15 -12
- package/CHANGELOG.md +26 -0
- package/CONTEXT.md +8 -6
- package/README.md +103 -53
- package/dist/actors/computer-use/loop/ending.js +4 -4
- package/dist/actors/local-agent/claude-participant.d.ts +74 -0
- package/dist/actors/local-agent/claude-participant.js +243 -0
- package/dist/actors/local-agent/claude-session.d.ts +16 -3
- package/dist/actors/local-agent/claude-session.js +64 -23
- package/dist/actors/local-agent/cli.d.ts +17 -0
- package/dist/actors/local-agent/cli.js +111 -24
- package/dist/actors/local-agent/readiness.d.ts +2 -2
- package/dist/actors/local-agent/readiness.js +20 -12
- package/dist/cli/commands/analyze.js +21 -5
- package/dist/cli/commands/observe.js +15 -2
- package/dist/cli/commands/run-signals.d.ts +2 -0
- package/dist/cli/commands/run-signals.js +106 -45
- package/dist/cli/commands/runs.d.ts +5 -0
- package/dist/cli/commands/runs.js +113 -74
- package/dist/cli/commands/setup.js +23 -14
- package/dist/cli/commands/study-route-computer-use.js +9 -3
- package/dist/cli/commands/study-route-open.js +1 -0
- package/dist/cli/commands/study-route-preview.js +1 -0
- package/dist/cli/commands/study-route-scripted.js +2 -0
- package/dist/cli/commands/study-route-shared-world.js +6 -2
- package/dist/cli/commands/study-route-terminal.js +2 -0
- package/dist/cli/commands/watch.js +14 -4
- package/dist/cli/doctor.d.ts +4 -1
- package/dist/cli/doctor.js +51 -10
- package/dist/cli/findings.d.ts +103 -0
- package/dist/cli/findings.js +317 -0
- package/dist/cli/first-run-path.d.ts +32 -11
- package/dist/cli/first-run-path.js +113 -69
- package/dist/cli/invocation.d.ts +7 -0
- package/dist/cli/invocation.js +32 -0
- package/dist/cli/io.d.ts +3 -1
- package/dist/cli/io.js +8 -15
- package/dist/cli/observer-follow.d.ts +20 -0
- package/dist/cli/observer-follow.js +36 -1
- package/dist/cli/orientation.js +16 -14
- package/dist/cli/product-sentence.d.ts +3 -3
- package/dist/cli/product-sentence.js +3 -3
- package/dist/cli/version.d.ts +1 -0
- package/dist/cli/version.js +11 -0
- package/dist/evidence/encoded-text.d.ts +9 -6
- package/dist/evidence/encoded-text.js +84 -19
- package/dist/evidence/redaction.d.ts +2 -0
- package/dist/evidence/redaction.js +123 -21
- package/dist/feedback/export-bundle.js +2 -2
- package/dist/keys/env-file.d.ts +4 -0
- package/dist/keys/env-file.js +17 -0
- package/dist/keys/key-resolution.js +6 -5
- package/dist/observer/data.d.ts +14 -1
- package/dist/observer/data.js +19 -9
- package/dist/observer/exposure.js +1 -1
- package/dist/observer/library.d.ts +4 -0
- package/dist/observer/library.js +21 -17
- package/dist/observer/render.js +10 -3
- package/dist/observer/run-routes.d.ts +2 -0
- package/dist/observer/run-routes.js +51 -35
- package/dist/observer/serve.js +2 -1
- package/dist/observer-app.html +3 -3
- package/dist/routes/computer-use/bundle.js +3 -1
- package/dist/routes/computer-use/fanout-records.js +1 -1
- package/dist/routes/computer-use/participant-execution.js +3 -1
- package/dist/routes/computer-use/participant-model.js +5 -2
- package/dist/routes/computer-use/plan.js +9 -4
- package/dist/routes/computer-use/preflight.js +1 -1
- package/dist/routes/computer-use/result.js +17 -28
- package/dist/routes/computer-use/single-bundle.js +7 -1
- package/dist/routes/scripted/bundle.js +4 -2
- package/dist/routes/scripted/plan.js +2 -2
- package/dist/routes/scripted/result.d.ts +3 -5
- package/dist/routes/scripted/result.js +16 -22
- package/dist/routes/scripted/scenario.js +4 -4
- package/dist/routes/shared-world/bundle-records.js +6 -1
- package/dist/routes/shared-world/bundle.js +2 -0
- package/dist/routes/shared-world/plan.js +6 -2
- package/dist/routes/shared-world/result.js +22 -22
- package/dist/routes/terminal/bundle.js +6 -3
- package/dist/routes/terminal/credentials.d.ts +5 -5
- package/dist/routes/terminal/credentials.js +14 -6
- package/dist/routes/terminal/dry-run.js +8 -12
- package/dist/routes/terminal/ledger.d.ts +3 -3
- package/dist/routes/terminal/ledger.js +6 -6
- package/dist/routes/terminal/live-finish.js +15 -13
- package/dist/routes/terminal/plan.js +12 -4
- package/dist/routes/terminal/result.d.ts +6 -7
- package/dist/routes/terminal/result.js +6 -22
- package/dist/routes/terminal/route.js +5 -4
- package/dist/routes/terminal/runtime-auth.d.ts +6 -0
- package/dist/routes/terminal/runtime-auth.js +5 -0
- package/dist/routes/terminal/session.js +1 -1
- package/dist/run/bundle-shape.d.ts +3 -1
- package/dist/run/bundle-shape.js +25 -1
- package/dist/run/bundle.d.ts +24 -0
- package/dist/run/display.d.ts +59 -0
- package/dist/run/display.js +106 -0
- package/dist/run/dry-run.js +13 -8
- package/dist/run/judge.d.ts +3 -2
- package/dist/run/judge.js +12 -1
- package/dist/run/local-only-files.d.ts +13 -0
- package/dist/run/local-only-files.js +35 -0
- package/dist/run/pricing.js +1 -1
- package/dist/run/projection.d.ts +0 -2
- package/dist/run/projection.js +0 -11
- package/dist/run/reclaim-outcomes.d.ts +89 -0
- package/dist/run/reclaim-outcomes.js +213 -0
- package/dist/run/reclaim.d.ts +23 -17
- package/dist/run/reclaim.js +213 -114
- package/dist/run/results.d.ts +10 -20
- package/dist/run/results.js +2 -2
- package/dist/run/run-index.d.ts +11 -1
- package/dist/run/run-index.js +11 -0
- package/dist/run/run-not-found.js +3 -2
- package/dist/run/run.d.ts +34 -18
- package/dist/run/run.js +166 -67
- package/dist/run/sandbox-creates.d.ts +59 -0
- package/dist/run/sandbox-creates.js +140 -0
- package/dist/run/sandbox-receipts.d.ts +20 -5
- package/dist/run/sandbox-receipts.js +56 -7
- package/dist/run/stats.d.ts +5 -0
- package/dist/run/stats.js +8 -4
- package/dist/run/status.d.ts +6 -9
- package/dist/run/status.js +7 -23
- package/dist/run/stored-runs.d.ts +5 -14
- package/dist/run/stored-runs.js +31 -146
- package/dist/run/streams.d.ts +3 -2
- package/dist/study/agents-section.d.ts +21 -0
- package/dist/study/agents-section.js +86 -0
- package/dist/study/doctor.d.ts +1 -1
- package/dist/study/doctor.js +8 -4
- package/dist/study/init-templates.js +0 -95
- package/dist/study/init.js +45 -37
- package/dist/study/keys.d.ts +12 -0
- package/dist/study/keys.js +32 -0
- package/dist/study/local-browser-host.d.ts +11 -0
- package/dist/study/local-browser-host.js +42 -0
- package/dist/study/parse/actors.js +9 -1
- package/dist/study/parse/subject.js +12 -0
- package/dist/study/parse/url-credentials.d.ts +9 -0
- package/dist/study/parse/url-credentials.js +50 -0
- package/dist/study/persona.d.ts +4 -0
- package/dist/study/persona.js +4 -2
- package/dist/study/preflight-probes.js +2 -1
- package/dist/study/types.d.ts +8 -6
- package/dist/study/url-credentials.d.ts +6 -0
- package/dist/study/url-credentials.js +34 -0
- package/dist/substrates/e2b/peer-install.d.ts +51 -0
- package/dist/substrates/e2b/peer-install.js +161 -0
- package/dist/substrates/e2b/sandbox.d.ts +34 -6
- package/dist/substrates/e2b/sandbox.js +141 -30
- package/dist/substrates/e2b/sdk.d.ts +33 -0
- package/dist/substrates/e2b/sdk.js +21 -21
- package/dist/substrates/local/firecracker-desktop.js +3 -2
- package/dist/substrates/local/runtime-host.js +5 -4
- package/dist/substrates/local/runtime.js +5 -4
- package/dist/substrates/shell.d.ts +2 -0
- package/dist/substrates/shell.js +4 -0
- package/dist/tui/contract.js +5 -4
- package/dist/tui-app.js +133 -133
- package/dist/verify/liveness.d.ts +19 -3
- package/dist/verify/liveness.js +47 -25
- package/dist/verify/verify.d.ts +3 -0
- package/dist/verify/verify.js +8 -3
- package/docs/architecture/actor-contract.md +2 -1
- package/docs/architecture/observer.md +22 -7
- package/docs/architecture/task-protocol-support.md +13 -13
- package/docs/architecture/terminal-product-route.md +15 -13
- package/docs/contracts/run-bundle.md +95 -10
- package/docs/contracts/schemas.md +44 -26
- package/docs/contracts/study-analysis.md +45 -0
- package/docs/contracts/study-costs.md +8 -0
- package/docs/decisions/0001-run-bundle-is-the-source-of-truth.md +3 -0
- package/docs/decisions/0003-contained-paths-and-cleanup-authority.md +9 -3
- package/docs/principles/invariants-and-defaults.md +11 -8
- package/package.json +6 -3
- package/skills/humanish/SKILL.md +38 -20
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// How a run reads wherever a surface says whether it passed: the TUI, the Observer and its run
|
|
2
|
+
// library, `humanish runs`, `humanish stats` and review.md. Each of them calls runDisplay with the
|
|
3
|
+
// same facts, so a run whose own ok is false shows as a pass on none of them, and two surfaces
|
|
4
|
+
// cannot disagree about one run.
|
|
5
|
+
import { classifyRunStatus, isRunStatusRecord } from "./status.js";
|
|
6
|
+
const LABELS = {
|
|
7
|
+
running: "running",
|
|
8
|
+
interrupted: "interrupted",
|
|
9
|
+
passed: "passed",
|
|
10
|
+
failed: "failed",
|
|
11
|
+
blocked: "blocked",
|
|
12
|
+
timed_out: "timed out",
|
|
13
|
+
no_verdict: "no verdict",
|
|
14
|
+
dry_run: "dry run",
|
|
15
|
+
unknown: "unknown",
|
|
16
|
+
};
|
|
17
|
+
const TONES = {
|
|
18
|
+
running: "live",
|
|
19
|
+
interrupted: "warn",
|
|
20
|
+
passed: "pass",
|
|
21
|
+
failed: "fail",
|
|
22
|
+
blocked: "warn",
|
|
23
|
+
timed_out: "warn",
|
|
24
|
+
no_verdict: "warn",
|
|
25
|
+
dry_run: "neutral",
|
|
26
|
+
unknown: "neutral",
|
|
27
|
+
};
|
|
28
|
+
function shown(state, reason) {
|
|
29
|
+
return {
|
|
30
|
+
state,
|
|
31
|
+
label: LABELS[state],
|
|
32
|
+
tone: TONES[state],
|
|
33
|
+
...(reason === undefined ? {} : { reason }),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A run's state. A live or interrupted run is that, whatever its last flush said. A finished run
|
|
38
|
+
* shows its participants' blocked, timed-out or failed verdict; otherwise a run whose ok is false
|
|
39
|
+
* is failed, even with a pass verdict, because its execution failed. Only then does a pass verdict
|
|
40
|
+
* show as passed.
|
|
41
|
+
*/
|
|
42
|
+
export function runDisplay(facts) {
|
|
43
|
+
if (facts.liveness === "running")
|
|
44
|
+
return shown("running");
|
|
45
|
+
if (facts.liveness === "interrupted")
|
|
46
|
+
return shown("interrupted");
|
|
47
|
+
const reason = facts.ok === false && facts.failure !== undefined
|
|
48
|
+
? `${facts.failure.kind}: ${facts.failure.message}`
|
|
49
|
+
: undefined;
|
|
50
|
+
if (facts.verdict === "blocked")
|
|
51
|
+
return shown("blocked", reason);
|
|
52
|
+
if (facts.verdict === "timed_out")
|
|
53
|
+
return shown("timed_out", reason);
|
|
54
|
+
if (facts.verdict === "fail" || facts.ok === false)
|
|
55
|
+
return shown("failed", reason);
|
|
56
|
+
if (facts.verdict === "pass")
|
|
57
|
+
return shown("passed");
|
|
58
|
+
if (facts.verdict === "contract_proof_only")
|
|
59
|
+
return shown(facts.mode === "dry-run" ? "dry_run" : "no_verdict");
|
|
60
|
+
return shown("unknown");
|
|
61
|
+
}
|
|
62
|
+
/** A state in words: "passed", "timed out", "no verdict". */
|
|
63
|
+
export function runDisplayLabel(state) {
|
|
64
|
+
return LABELS[state];
|
|
65
|
+
}
|
|
66
|
+
/** A display as one line of text: the label, then why the run failed when it says. */
|
|
67
|
+
function runDisplayLine(display) {
|
|
68
|
+
return display.reason === undefined ? display.label : `${display.label}: ${display.reason}`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The facts for a run from its bundle. run.json's `outcome` decides when it has one. A bundle
|
|
72
|
+
* without one is in progress, or was recorded before the field existed: its status record, when
|
|
73
|
+
* given and naming the run, then says whether the run is alive and, for the older run, holds its ok.
|
|
74
|
+
* With no record, a bundle whose simulations are still running is live.
|
|
75
|
+
*/
|
|
76
|
+
export function bundleDisplayFacts(bundle, status, nowMs = Date.now()) {
|
|
77
|
+
const base = { mode: bundle.mode, verdict: bundle.review?.verdict };
|
|
78
|
+
const outcome = bundle.outcome;
|
|
79
|
+
if (outcome?.state === "interrupted")
|
|
80
|
+
return { ...base, liveness: "interrupted" };
|
|
81
|
+
if (outcome?.state === "finished")
|
|
82
|
+
return {
|
|
83
|
+
...base,
|
|
84
|
+
liveness: "finished",
|
|
85
|
+
ok: outcome.ok,
|
|
86
|
+
failure: outcome.execution.failures[0],
|
|
87
|
+
};
|
|
88
|
+
if (isRunStatusRecord(status) && status.runId === bundle.runId)
|
|
89
|
+
return {
|
|
90
|
+
...base,
|
|
91
|
+
liveness: classifyRunStatus(status, nowMs),
|
|
92
|
+
...(typeof status.outcome?.ok === "boolean" ? { ok: status.outcome.ok } : {}),
|
|
93
|
+
...(status.outcome?.execution?.failures[0] === undefined
|
|
94
|
+
? {}
|
|
95
|
+
: { failure: status.outcome.execution.failures[0] }),
|
|
96
|
+
};
|
|
97
|
+
const inProgress = (bundle.simulations ?? []).some((simulation) => simulation?.status === "running");
|
|
98
|
+
return { ...base, liveness: inProgress ? "running" : "finished" };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* How review.md says the run ended: the display of the bundle it is rendered from, so a run whose
|
|
102
|
+
* execution failed never reads as a pass beside its participants' verdict.
|
|
103
|
+
*/
|
|
104
|
+
export function reviewOutcome(bundle) {
|
|
105
|
+
return runDisplayLine(runDisplay(bundleDisplayFacts(bundle)));
|
|
106
|
+
}
|
package/dist/run/dry-run.js
CHANGED
|
@@ -10,6 +10,8 @@ import { participantEvent, participantIdsOf, participantRecord, participantStrea
|
|
|
10
10
|
import { implicitProjectDirectoryExists, readPackageName, validateCwd } from "./project.js";
|
|
11
11
|
import { loadDryRunInputs } from "./dry-run-inputs.js";
|
|
12
12
|
import { judgeExecution, judgePreview, OUTCOME_POLICIES, resultOk, verdictText, } from "./judge.js";
|
|
13
|
+
import { reviewOutcome } from "./display.js";
|
|
14
|
+
import { cli } from "../cli/invocation.js";
|
|
13
15
|
/**
|
|
14
16
|
* The preview route's run. The run scope closes the run it started on every exit, including
|
|
15
17
|
* the fail-closed ones. It renders an Observer only when `options.observer` asks for one.
|
|
@@ -48,7 +50,7 @@ async function runDryRunInScope(options, scope) {
|
|
|
48
50
|
if (!options.dryRun) {
|
|
49
51
|
return refused(requestedCwd, warnings, {
|
|
50
52
|
code: "HUMANISH_LIVE_RUN_UNIMPLEMENTED",
|
|
51
|
-
message:
|
|
53
|
+
message: `humanish run needs a study. List studies with ${cli("study list")}, or run ${cli("run --dry-run")} for a sample bundle.`,
|
|
52
54
|
});
|
|
53
55
|
}
|
|
54
56
|
const createdAt = new Date().toISOString();
|
|
@@ -123,16 +125,17 @@ async function runDryRunInScope(options, scope) {
|
|
|
123
125
|
review: createReviewSummary(judgment.verdict),
|
|
124
126
|
feedbackCandidates: [],
|
|
125
127
|
};
|
|
126
|
-
const finished = await run.finish(bundle);
|
|
127
|
-
const observer = options.observer === undefined ? undefined : await renderPreviewObserver(finished, warnings);
|
|
128
128
|
// The preview's policy keeps an Observer that did not render a warning: the bundle is still
|
|
129
129
|
// evidence, and `export` can render it later.
|
|
130
130
|
const policy = OUTCOME_POLICIES.preview;
|
|
131
|
-
const execution = judgeExecution(
|
|
132
|
-
|
|
133
|
-
:
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
const execution = judgeExecution([], policy);
|
|
132
|
+
const finished = await run.finish(bundle, {
|
|
133
|
+
ok: resultOk({ judgment, execution, scorerFailures: [], policy }),
|
|
134
|
+
execution,
|
|
135
|
+
policy,
|
|
136
|
+
});
|
|
137
|
+
const observer = options.observer === undefined ? undefined : await renderPreviewObserver(finished, warnings);
|
|
138
|
+
const { ok } = finished.outcome;
|
|
136
139
|
return {
|
|
137
140
|
schema: "humanish.run-result.v1",
|
|
138
141
|
ok,
|
|
@@ -175,6 +178,8 @@ Mode: ${bundle.mode}
|
|
|
175
178
|
|
|
176
179
|
Verdict: ${verdictText(bundle.review.verdict, bundle.mode)}
|
|
177
180
|
|
|
181
|
+
Outcome: ${reviewOutcome(bundle)}
|
|
182
|
+
|
|
178
183
|
${bundle.review.summary}
|
|
179
184
|
|
|
180
185
|
## Public-Safety
|
package/dist/run/judge.d.ts
CHANGED
|
@@ -91,8 +91,9 @@ export interface Judgment {
|
|
|
91
91
|
}
|
|
92
92
|
/** The judgment for a verdict: a run passes when its verdict is pass, and a dry run passes. */
|
|
93
93
|
export declare function judgmentOf(verdict: Verdict, dryRun: boolean): Judgment;
|
|
94
|
-
/**
|
|
95
|
-
|
|
94
|
+
/** The kinds of execution failure a run records. */
|
|
95
|
+
export declare const EXECUTION_FAILURE_KINDS: readonly ["harness", "provider-cleanup", "provider-policy", "sandbox-cleanup", "evidence", "cap", "run"];
|
|
96
|
+
type ExecutionFailureKind = (typeof EXECUTION_FAILURE_KINDS)[number];
|
|
96
97
|
/** One way the run failed as an execution. The message is scrubbed; status.json shows it. */
|
|
97
98
|
export interface ExecutionFailure {
|
|
98
99
|
kind: ExecutionFailureKind;
|
package/dist/run/judge.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// 4. foldScorerFailures, which every route with a scorer applies before the run finishes;
|
|
13
13
|
// 5. the outcome: judgeExecution applies the route's policy to its execution failures, and
|
|
14
14
|
// resultOk gives the result's ok.
|
|
15
|
+
import { cli } from "../cli/invocation.js";
|
|
15
16
|
/**
|
|
16
17
|
* The participant said it reached the goal having taken no action and said nothing, and no stop
|
|
17
18
|
* condition ended the session. It most likely saw a blank or loading screen, so it is not a pass.
|
|
@@ -108,6 +109,16 @@ export function verdictForStatus(status) {
|
|
|
108
109
|
export function judgmentOf(verdict, dryRun) {
|
|
109
110
|
return { verdict, passed: dryRun || verdict === "pass" };
|
|
110
111
|
}
|
|
112
|
+
/** The kinds of execution failure a run records. */
|
|
113
|
+
export const EXECUTION_FAILURE_KINDS = [
|
|
114
|
+
"harness",
|
|
115
|
+
"provider-cleanup",
|
|
116
|
+
"provider-policy",
|
|
117
|
+
"sandbox-cleanup",
|
|
118
|
+
"evidence",
|
|
119
|
+
"cap",
|
|
120
|
+
"run",
|
|
121
|
+
];
|
|
111
122
|
/**
|
|
112
123
|
* The execution failure for one sandbox whose release is unconfirmed. `warning` is the route's
|
|
113
124
|
* release warning, already scrubbed. `recovery` says how to release it by hand; without one,
|
|
@@ -116,7 +127,7 @@ export function judgmentOf(verdict, dryRun) {
|
|
|
116
127
|
export function sandboxCleanupFailure(owner, warning, runId, recovery) {
|
|
117
128
|
return {
|
|
118
129
|
kind: "sandbox-cleanup",
|
|
119
|
-
message: `${owner}: ${warning ?? "Sandbox release is unconfirmed."} ${recovery ?? `Reclaim it by recorded id with
|
|
130
|
+
message: `${owner}: ${warning ?? "Sandbox release is unconfirmed."} ${recovery ?? `Reclaim it by recorded id with \`${cli(`reclaim --run ${runId}`)}\`.`}`,
|
|
120
131
|
};
|
|
121
132
|
}
|
|
122
133
|
/** Each route's outcome policy, in one place. */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Each local-only file, relative to the run directory, with why a shared copy omits it. */
|
|
2
|
+
export declare const LOCAL_ONLY_RUN_FILES: ReadonlyMap<string, string>;
|
|
3
|
+
/**
|
|
4
|
+
* Whether a run-relative posix path names a local-only file. Case is ignored, so a request that
|
|
5
|
+
* spells the name differently cannot reach the file on a case-insensitive filesystem.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isLocalOnlyRunFile(relativePath: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Whether `filePath` is one of the local-only files of the run at `runRoot` under another
|
|
10
|
+
* spelling: the same device and inode. A case-insensitive filesystem also folds Unicode, so a name
|
|
11
|
+
* check alone cannot rule every spelling out.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isLocalOnlyRunFileIdentity(runRoot: string, filePath: string): Promise<boolean>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Run files that mean something only on the machine that recorded the run. sandbox-receipts.ndjson
|
|
2
|
+
// holds the raw sandbox ids reclaim kills by, and status.json holds the recording process's pid.
|
|
3
|
+
// Bundle export leaves both out of a shared copy, and the Observer servers never hand them out.
|
|
4
|
+
import { lstat } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { SANDBOX_RECEIPTS_ARTIFACT } from "./sandbox-receipts.js";
|
|
7
|
+
import { RUN_STATUS_FILE } from "./status.js";
|
|
8
|
+
/** Each local-only file, relative to the run directory, with why a shared copy omits it. */
|
|
9
|
+
export const LOCAL_ONLY_RUN_FILES = new Map([
|
|
10
|
+
[RUN_STATUS_FILE, "local process status is not a new attempt"],
|
|
11
|
+
[SANDBOX_RECEIPTS_ARTIFACT, "operational journal does not confer a derivative resource lease"],
|
|
12
|
+
]);
|
|
13
|
+
/**
|
|
14
|
+
* Whether a run-relative posix path names a local-only file. Case is ignored, so a request that
|
|
15
|
+
* spells the name differently cannot reach the file on a case-insensitive filesystem.
|
|
16
|
+
*/
|
|
17
|
+
export function isLocalOnlyRunFile(relativePath) {
|
|
18
|
+
return LOCAL_ONLY_RUN_FILES.has(relativePath.toLowerCase());
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Whether `filePath` is one of the local-only files of the run at `runRoot` under another
|
|
22
|
+
* spelling: the same device and inode. A case-insensitive filesystem also folds Unicode, so a name
|
|
23
|
+
* check alone cannot rule every spelling out.
|
|
24
|
+
*/
|
|
25
|
+
export async function isLocalOnlyRunFileIdentity(runRoot, filePath) {
|
|
26
|
+
const requested = await lstat(filePath, { bigint: true }).catch(() => null);
|
|
27
|
+
if (requested === null)
|
|
28
|
+
return false;
|
|
29
|
+
for (const name of LOCAL_ONLY_RUN_FILES.keys()) {
|
|
30
|
+
const local = await lstat(path.join(runRoot, name), { bigint: true }).catch(() => null);
|
|
31
|
+
if (local !== null && local.dev === requested.dev && local.ino === requested.ino)
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
package/dist/run/pricing.js
CHANGED
|
@@ -101,7 +101,7 @@ const DESKTOP_RESOURCE_RATE = {
|
|
|
101
101
|
* since an npm install cannot edit this file.
|
|
102
102
|
*/
|
|
103
103
|
export function unpricedCapMessage(model) {
|
|
104
|
-
return `
|
|
104
|
+
return `caps sets a spend cap (maxUsd or maxTotalUsd), but humanish has no rate for model "${model}", so it cannot enforce the cap and refuses to start. Priced models: ${Object.keys(MODEL_RATES).sort().join(", ")}. Use one of them, or remove maxUsd and maxTotalUsd to run without a cap.`;
|
|
105
105
|
}
|
|
106
106
|
export const DESKTOP_RATE = {
|
|
107
107
|
usdPerMinute: 0.00888,
|
package/dist/run/projection.d.ts
CHANGED
|
@@ -99,8 +99,6 @@ export declare function listWindow(args: {
|
|
|
99
99
|
start: number;
|
|
100
100
|
end: number;
|
|
101
101
|
};
|
|
102
|
-
/** The label for a run's state, in the register the surfaces share. */
|
|
103
|
-
export declare function livenessLabel(entry: Pick<RunIndexEntry, "liveness" | "verdict">): string;
|
|
104
102
|
/** A study as the study list shows it: what is declared, joined to what actually happened. */
|
|
105
103
|
export interface StudyRow {
|
|
106
104
|
/**
|
package/dist/run/projection.js
CHANGED
|
@@ -228,17 +228,6 @@ export function listWindow(args) {
|
|
|
228
228
|
start = Math.max(0, Math.min(start, total - viewport));
|
|
229
229
|
return { start, end: start + viewport };
|
|
230
230
|
}
|
|
231
|
-
/** The label for a run's state, in the register the surfaces share. */
|
|
232
|
-
export function livenessLabel(entry) {
|
|
233
|
-
switch (entry.liveness) {
|
|
234
|
-
case "running":
|
|
235
|
-
return "running";
|
|
236
|
-
case "interrupted":
|
|
237
|
-
return "interrupted";
|
|
238
|
-
default:
|
|
239
|
-
return entry.verdict ?? "finished";
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
231
|
/** The addressable handle for a manifest: its filename without directory or extension. */
|
|
243
232
|
function studyNameFromPath(manifestPath) {
|
|
244
233
|
const base = manifestPath.split("/").pop() ?? manifestPath;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type E2BTagSearch } from "../substrates/e2b/sandbox.js";
|
|
2
|
+
import type { E2BDesktopModule } from "../substrates/e2b/sdk.js";
|
|
3
|
+
import { type PreparedOutputRoot } from "./contained-output.js";
|
|
4
|
+
export declare const RECLAIM_RESULT_SCHEMA = "humanish.reclaim-result.v1";
|
|
5
|
+
export declare const RECLAIM_RECEIPT_ARTIFACT = "reclaim-receipt.json";
|
|
6
|
+
/**
|
|
7
|
+
* killed = kill(id) confirmed; already-gone = the server no longer knows the id (TTL or a prior
|
|
8
|
+
* cleanup got it), or the run's route released it before the signal; kill-failed = the attempt
|
|
9
|
+
* errored (the TTL backstop still applies); unsupported-provider = the receipt names a provider
|
|
10
|
+
* this version cannot reach, so nothing was attempted; pending = the kill had not answered when
|
|
11
|
+
* the receipt was written. `--check` writes running, already-gone or check-failed instead.
|
|
12
|
+
*/
|
|
13
|
+
export type ReclaimOutcomeState = "killed" | "already-gone" | "kill-failed" | "unsupported-provider" | "pending" | "running" | "check-failed";
|
|
14
|
+
export interface ReclaimOutcome {
|
|
15
|
+
/** "[redacted-sandbox-id]"; the raw id is only in the run's sandbox-receipts.ndjson. */
|
|
16
|
+
sandboxId: string;
|
|
17
|
+
/** The id's digest, which matches its receipt. */
|
|
18
|
+
sandboxIdDigest: string;
|
|
19
|
+
laneId: string;
|
|
20
|
+
/** How reclaim learned of the sandbox: its receipt, a create in this process whose receipt had
|
|
21
|
+
* not landed, or E2B's list of sandboxes tagged with this run. */
|
|
22
|
+
source: "receipt" | "create" | "tag";
|
|
23
|
+
state: ReclaimOutcomeState;
|
|
24
|
+
detail?: string;
|
|
25
|
+
}
|
|
26
|
+
/** E2B's list of sandboxes carrying the run's owner tags. */
|
|
27
|
+
export interface ReclaimTagSearch {
|
|
28
|
+
/** not-run: the search did not start, because an earlier step failed or the run was stopping. */
|
|
29
|
+
status: E2BTagSearch["status"] | "not-run";
|
|
30
|
+
/** Tagged sandboxes no receipt or create named. */
|
|
31
|
+
found: number;
|
|
32
|
+
detail?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* clean: every sandbox found is confirmed gone, and the tag search finished with no create still
|
|
36
|
+
* in flight. unconfirmed: a kill did not confirm. running (`--check` only): a sandbox still
|
|
37
|
+
* exists. unknown: nothing failed, but reclaim could not search E2B or a create was still in
|
|
38
|
+
* flight, so a sandbox it never saw may still run.
|
|
39
|
+
*/
|
|
40
|
+
export type ReclaimState = "clean" | "unconfirmed" | "running" | "unknown";
|
|
41
|
+
/** One sandbox reclaim acts on, keyed by provider and raw id. */
|
|
42
|
+
export interface Target {
|
|
43
|
+
provider: string;
|
|
44
|
+
rawId: string;
|
|
45
|
+
outcome: ReclaimOutcome;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The sandboxes one reclaim acts on, each once by provider and raw id. The receipt naming every
|
|
49
|
+
* sandbox found, each pending, lands before that sandbox's kill starts, so an exit during the
|
|
50
|
+
* kills leaves a record of each one not yet confirmed gone.
|
|
51
|
+
*/
|
|
52
|
+
export declare function targetSet(options: {
|
|
53
|
+
e2b: E2BDesktopModule | undefined;
|
|
54
|
+
check: boolean;
|
|
55
|
+
requestTimeoutMs: number;
|
|
56
|
+
earlier: ReclaimOutcome[];
|
|
57
|
+
persist: ReturnType<typeof receiptWriter>;
|
|
58
|
+
}): {
|
|
59
|
+
snapshot: (carried?: ReclaimOutcome[]) => ReclaimOutcome[];
|
|
60
|
+
/** Add one sandbox; false when it was already in the set. */
|
|
61
|
+
add(provider: string, rawId: string, participant: string, source: ReclaimOutcome["source"], released?: boolean): boolean;
|
|
62
|
+
/** Write the receipt naming every sandbox found so far, then start their kills. */
|
|
63
|
+
start(): Promise<void>;
|
|
64
|
+
/** Wait for every kill, including ones added while waiting. */
|
|
65
|
+
drain(): Promise<void>;
|
|
66
|
+
finish(outcomes: readonly ReclaimOutcome[], state: ReclaimState): Promise<void>;
|
|
67
|
+
};
|
|
68
|
+
/** Search E2B by each distinct tag set; done only when every search finished. */
|
|
69
|
+
export declare function searchTagSets(e2b: E2BDesktopModule | undefined, tagSets: readonly Record<string, string>[], options: {
|
|
70
|
+
requestTimeoutMs: number;
|
|
71
|
+
unavailable: string;
|
|
72
|
+
}): Promise<E2BTagSearch>;
|
|
73
|
+
export declare const GONE: Set<ReclaimOutcomeState>;
|
|
74
|
+
/**
|
|
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; an unreadable or foreign receipt carries nothing.
|
|
77
|
+
*/
|
|
78
|
+
export declare function earlierOutcomes(root: PreparedOutputRoot, label: string): Promise<ReclaimOutcome[]>;
|
|
79
|
+
export declare function rootState(outcomes: readonly ReclaimOutcome[], check: boolean, searched: boolean): ReclaimState;
|
|
80
|
+
/** Resolves when `settled` does, or after `ms` if given, whichever is first. */
|
|
81
|
+
export declare function settledWithin(settled: Promise<void>, ms: number | undefined): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Writes reclaim-receipt.json in order, one write at a time, each with the latest outcomes. A
|
|
84
|
+
* check writes nothing. A failed write adds one warning and leaves the result unchanged.
|
|
85
|
+
*/
|
|
86
|
+
export declare function receiptWriter(root: PreparedOutputRoot, runId: string, receiptCount: number, check: boolean, warnings: string[]): {
|
|
87
|
+
/** Queue one write with these outcomes; resolves once it has landed or failed. */
|
|
88
|
+
write(outcomes: readonly ReclaimOutcome[], state: ReclaimState): Promise<void>;
|
|
89
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// What one reclaim found and did, sandbox by sandbox: the outcome vocabulary of
|
|
2
|
+
// reclaim-receipt.json, the set of sandboxes a reclaim acts on (each once, with its pending record
|
|
3
|
+
// written before its kill starts), the tag search across the tag sets a run recorded, and the
|
|
4
|
+
// receipt writer. src/run/reclaim.ts decides what to find; this module records what happened.
|
|
5
|
+
import { REDACTED_SANDBOX_ID, redactText, sandboxIdDigest, toErrorMessage, } from "../evidence/redaction.js";
|
|
6
|
+
import { destroyE2BSandbox, findE2BSandboxesByTags, inspectE2BSandbox, } from "../substrates/e2b/sandbox.js";
|
|
7
|
+
import { readContainedRegularFile, writeContainedOutputFile, } from "./contained-output.js";
|
|
8
|
+
import { scrubSandboxIds } from "./sandbox-ids.js";
|
|
9
|
+
import { isRecord } from "./type-guards.js";
|
|
10
|
+
export const RECLAIM_RESULT_SCHEMA = "humanish.reclaim-result.v1";
|
|
11
|
+
export const RECLAIM_RECEIPT_ARTIFACT = "reclaim-receipt.json";
|
|
12
|
+
/**
|
|
13
|
+
* The sandboxes one reclaim acts on, each once by provider and raw id. The receipt naming every
|
|
14
|
+
* sandbox found, each pending, lands before that sandbox's kill starts, so an exit during the
|
|
15
|
+
* kills leaves a record of each one not yet confirmed gone.
|
|
16
|
+
*/
|
|
17
|
+
export function targetSet(options) {
|
|
18
|
+
const { e2b, check, requestTimeoutMs, earlier, persist } = options;
|
|
19
|
+
const targets = new Map();
|
|
20
|
+
const work = [];
|
|
21
|
+
const queued = [];
|
|
22
|
+
let started = false;
|
|
23
|
+
const snapshot = (carried = earlier) => {
|
|
24
|
+
const current = [...targets.values()].map((target) => target.outcome);
|
|
25
|
+
const seen = new Set(current.map((outcome) => outcome.sandboxIdDigest));
|
|
26
|
+
return [...current, ...carried.filter((outcome) => !seen.has(outcome.sandboxIdDigest))];
|
|
27
|
+
};
|
|
28
|
+
const act = (target) => actOn(e2b, target, { check, requestTimeoutMs }).then((settled) => {
|
|
29
|
+
target.outcome = settled;
|
|
30
|
+
void persist.write(snapshot(), "unknown");
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
snapshot,
|
|
34
|
+
/** Add one sandbox; false when it was already in the set. */
|
|
35
|
+
add(provider, rawId, participant, source, released = false) {
|
|
36
|
+
const key = JSON.stringify([provider, rawId]);
|
|
37
|
+
if (targets.has(key))
|
|
38
|
+
return false;
|
|
39
|
+
const pending = outcomeFor(sandboxIdDigest(rawId), participant, source);
|
|
40
|
+
const target = {
|
|
41
|
+
provider,
|
|
42
|
+
rawId,
|
|
43
|
+
outcome: released
|
|
44
|
+
? { ...pending, state: "already-gone", detail: "its route released it before the stop" }
|
|
45
|
+
: pending,
|
|
46
|
+
};
|
|
47
|
+
targets.set(key, target);
|
|
48
|
+
if (released)
|
|
49
|
+
return true;
|
|
50
|
+
if (started)
|
|
51
|
+
work.push(persist.write(snapshot(), "unknown").then(() => act(target)));
|
|
52
|
+
else
|
|
53
|
+
queued.push(target);
|
|
54
|
+
return true;
|
|
55
|
+
},
|
|
56
|
+
/** Write the receipt naming every sandbox found so far, then start their kills. */
|
|
57
|
+
async start() {
|
|
58
|
+
let size;
|
|
59
|
+
do {
|
|
60
|
+
size = targets.size;
|
|
61
|
+
await persist.write(snapshot(), "unknown");
|
|
62
|
+
} while (targets.size !== size);
|
|
63
|
+
started = true;
|
|
64
|
+
for (const target of queued)
|
|
65
|
+
work.push(act(target));
|
|
66
|
+
},
|
|
67
|
+
/** Wait for every kill, including ones added while waiting. */
|
|
68
|
+
async drain() {
|
|
69
|
+
let count;
|
|
70
|
+
do {
|
|
71
|
+
count = work.length;
|
|
72
|
+
await Promise.all(work);
|
|
73
|
+
} while (work.length !== count);
|
|
74
|
+
},
|
|
75
|
+
async finish(outcomes, state) {
|
|
76
|
+
await persist.write(outcomes, state);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Search E2B by each distinct tag set; done only when every search finished. */
|
|
81
|
+
export async function searchTagSets(e2b, tagSets, options) {
|
|
82
|
+
if (e2b === undefined)
|
|
83
|
+
return { status: "unavailable", sandboxes: [], detail: options.unavailable };
|
|
84
|
+
const distinct = new Map(tagSets.map((tags) => [
|
|
85
|
+
JSON.stringify(Object.entries(tags).sort(([a], [b]) => a.localeCompare(b))),
|
|
86
|
+
tags,
|
|
87
|
+
]));
|
|
88
|
+
const searches = await Promise.all([...distinct.values()].map((tags) => findE2BSandboxesByTags(e2b, tags, { requestTimeoutMs: options.requestTimeoutMs })));
|
|
89
|
+
const unfinished = searches.find((search) => search.status !== "done");
|
|
90
|
+
const sandboxes = new Map(searches.flatMap((search) => search.sandboxes).map((listed) => [listed.sandboxId, listed]));
|
|
91
|
+
return {
|
|
92
|
+
status: unfinished?.status ?? "done",
|
|
93
|
+
sandboxes: [...sandboxes.values()],
|
|
94
|
+
...(unfinished?.detail === undefined ? {} : { detail: unfinished.detail }),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** Kill (or look up) one sandbox, and say what happened in reclaim's vocabulary. */
|
|
98
|
+
async function actOn(e2b, target, options) {
|
|
99
|
+
const { outcome, provider, rawId } = target;
|
|
100
|
+
if (provider !== "e2b" || e2b === undefined)
|
|
101
|
+
return {
|
|
102
|
+
...outcome,
|
|
103
|
+
state: "unsupported-provider",
|
|
104
|
+
detail: `provider ${JSON.stringify(provider)} is not supported by this humanish version; nothing was attempted, and that provider's own timeout is the backstop`,
|
|
105
|
+
};
|
|
106
|
+
const attempt = options.check
|
|
107
|
+
? await inspectE2BSandbox(e2b, rawId, options)
|
|
108
|
+
: await destroyE2BSandbox(e2b, rawId, options);
|
|
109
|
+
const { detail: _previous, ...rest } = outcome;
|
|
110
|
+
// A provider error that quotes the id is scrubbed; the raw id stays in sandbox-receipts.ndjson.
|
|
111
|
+
return "detail" in attempt
|
|
112
|
+
? { ...rest, state: attempt.state, detail: scrubSandboxIds(attempt.detail, [rawId]) }
|
|
113
|
+
: { ...rest, state: attempt.state };
|
|
114
|
+
}
|
|
115
|
+
export const GONE = new Set(["killed", "already-gone"]);
|
|
116
|
+
const OUTCOME_STATES = new Set([
|
|
117
|
+
"killed",
|
|
118
|
+
"already-gone",
|
|
119
|
+
"kill-failed",
|
|
120
|
+
"unsupported-provider",
|
|
121
|
+
"pending",
|
|
122
|
+
]);
|
|
123
|
+
/**
|
|
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; an unreadable or foreign receipt carries nothing.
|
|
126
|
+
*/
|
|
127
|
+
export async function earlierOutcomes(root, label) {
|
|
128
|
+
let parsed;
|
|
129
|
+
try {
|
|
130
|
+
const bytes = await readContainedRegularFile(root, RECLAIM_RECEIPT_ARTIFACT);
|
|
131
|
+
if (bytes === null)
|
|
132
|
+
return [];
|
|
133
|
+
parsed = JSON.parse(bytes.toString("utf8"));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
if (!isRecord(parsed) || parsed.runId !== label || !Array.isArray(parsed.outcomes))
|
|
139
|
+
return [];
|
|
140
|
+
const carried = [];
|
|
141
|
+
for (const entry of parsed.outcomes) {
|
|
142
|
+
if (!isRecord(entry))
|
|
143
|
+
continue;
|
|
144
|
+
const text = (key) => typeof entry[key] === "string" ? entry[key] : undefined;
|
|
145
|
+
const state = text("state");
|
|
146
|
+
const raw = text("sandboxId");
|
|
147
|
+
const digest = text("sandboxIdDigest") ??
|
|
148
|
+
(raw === undefined || raw === REDACTED_SANDBOX_ID ? undefined : sandboxIdDigest(raw));
|
|
149
|
+
if (digest === undefined || state === undefined || !OUTCOME_STATES.has(state))
|
|
150
|
+
continue;
|
|
151
|
+
const source = text("source");
|
|
152
|
+
const detail = text("detail");
|
|
153
|
+
carried.push({
|
|
154
|
+
...outcomeFor(digest, text("laneId") ?? "unknown", source === "create" || source === "tag" ? source : "receipt"),
|
|
155
|
+
state: state,
|
|
156
|
+
...(detail === undefined ? {} : { detail }),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return carried;
|
|
160
|
+
}
|
|
161
|
+
/** A pending outcome, naming the sandbox by digest only. */
|
|
162
|
+
function outcomeFor(digest, participant, source) {
|
|
163
|
+
return {
|
|
164
|
+
sandboxId: REDACTED_SANDBOX_ID,
|
|
165
|
+
sandboxIdDigest: digest,
|
|
166
|
+
laneId: participant,
|
|
167
|
+
source,
|
|
168
|
+
state: "pending",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export function rootState(outcomes, check, searched) {
|
|
172
|
+
if (check && outcomes.some((outcome) => outcome.state === "running"))
|
|
173
|
+
return "running";
|
|
174
|
+
const gone = outcomes.every((outcome) => GONE.has(outcome.state));
|
|
175
|
+
if (!gone)
|
|
176
|
+
return check ? "unknown" : "unconfirmed";
|
|
177
|
+
return searched ? "clean" : "unknown";
|
|
178
|
+
}
|
|
179
|
+
/** Resolves when `settled` does, or after `ms` if given, whichever is first. */
|
|
180
|
+
export async function settledWithin(settled, ms) {
|
|
181
|
+
if (ms === undefined)
|
|
182
|
+
return settled;
|
|
183
|
+
let timer;
|
|
184
|
+
await Promise.race([
|
|
185
|
+
settled,
|
|
186
|
+
new Promise((resolve) => {
|
|
187
|
+
timer = setTimeout(resolve, ms);
|
|
188
|
+
}),
|
|
189
|
+
]).finally(() => clearTimeout(timer));
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Writes reclaim-receipt.json in order, one write at a time, each with the latest outcomes. A
|
|
193
|
+
* check writes nothing. A failed write adds one warning and leaves the result unchanged.
|
|
194
|
+
*/
|
|
195
|
+
export function receiptWriter(root, runId, receiptCount, check, warnings) {
|
|
196
|
+
let chain = Promise.resolve();
|
|
197
|
+
let failed = false;
|
|
198
|
+
return {
|
|
199
|
+
/** Queue one write with these outcomes; resolves once it has landed or failed. */
|
|
200
|
+
write(outcomes, state) {
|
|
201
|
+
if (check)
|
|
202
|
+
return chain;
|
|
203
|
+
const text = `${JSON.stringify({ schema: RECLAIM_RESULT_SCHEMA, at: new Date().toISOString(), runId, state, receiptCount, outcomes }, null, 2)}\n`;
|
|
204
|
+
chain = chain.then(() => writeContainedOutputFile(root, RECLAIM_RECEIPT_ARTIFACT, text, "utf8").catch((error) => {
|
|
205
|
+
if (failed)
|
|
206
|
+
return;
|
|
207
|
+
failed = true;
|
|
208
|
+
warnings.push(`Reclaim ran but its receipt could not be written: ${redactText(toErrorMessage(error))}`);
|
|
209
|
+
}));
|
|
210
|
+
return chain;
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
package/dist/run/reclaim.d.ts
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
import { type E2BDesktopModule } from "../substrates/e2b/sdk.js";
|
|
2
|
+
import { type StoppedSandboxCreates } from "./sandbox-creates.js";
|
|
3
|
+
import { RECLAIM_RECEIPT_ARTIFACT, RECLAIM_RESULT_SCHEMA, type ReclaimOutcome, type ReclaimState, type ReclaimTagSearch } from "./reclaim-outcomes.js";
|
|
2
4
|
import { type PreparedRunArtifactPaths } from "./paths.js";
|
|
3
|
-
|
|
4
|
-
export declare const RECLAIM_RECEIPT_ARTIFACT = "reclaim-receipt.json";
|
|
5
|
-
interface ReclaimOutcome {
|
|
6
|
-
/** "[redacted-sandbox-id]"; the raw id is only in the run's sandbox-receipts.ndjson. */
|
|
7
|
-
sandboxId: string;
|
|
8
|
-
/** The id's digest, which matches its receipt. */
|
|
9
|
-
sandboxIdDigest: string;
|
|
10
|
-
laneId: string;
|
|
11
|
-
/** killed = kill(id) confirmed; already-gone = the server no longer knows the id (TTL or a
|
|
12
|
-
* prior cleanup got it); kill-failed = the attempt errored (the TTL backstop still applies);
|
|
13
|
-
* unsupported-provider = the receipt names a provider this version cannot reach, so nothing
|
|
14
|
-
* was attempted. */
|
|
15
|
-
state: "killed" | "already-gone" | "kill-failed" | "unsupported-provider";
|
|
16
|
-
detail?: string;
|
|
17
|
-
}
|
|
5
|
+
export { RECLAIM_RECEIPT_ARTIFACT };
|
|
18
6
|
export interface ReclaimResult {
|
|
19
7
|
schema: typeof RECLAIM_RESULT_SCHEMA;
|
|
8
|
+
/** True only when `state` is clean. */
|
|
20
9
|
ok: boolean;
|
|
10
|
+
state: ReclaimState;
|
|
11
|
+
/** `check` kills nothing and writes nothing. */
|
|
12
|
+
mode: "kill" | "check";
|
|
21
13
|
cwd: string;
|
|
22
14
|
runId: string;
|
|
23
|
-
/** Receipts found in the run's journal (0 = nothing
|
|
15
|
+
/** Receipts found in the run's journal (0 = nothing journaled, or the run predates receipts). */
|
|
24
16
|
receiptCount: number;
|
|
25
17
|
outcomes: ReclaimOutcome[];
|
|
18
|
+
tagSearch: ReclaimTagSearch;
|
|
19
|
+
/** Creates that had not returned when reclaim finished; only the signal handler sees any. */
|
|
20
|
+
createsInFlight: number;
|
|
21
|
+
/**
|
|
22
|
+
* Why there was nothing to look for: `dry-run` when run.json or status.json records the run as
|
|
23
|
+
* a dry run, which creates no sandboxes. Reclaim then contacts E2B for nothing.
|
|
24
|
+
*/
|
|
25
|
+
reason?: "dry-run";
|
|
26
26
|
warnings: string[];
|
|
27
27
|
error?: {
|
|
28
28
|
code: "HUMANISH_RECLAIM_RUN_NOT_FOUND" | "HUMANISH_RECLAIM_E2B_DEBUG" | "HUMANISH_RECLAIM_MODULE_UNAVAILABLE" | "HUMANISH_RECLAIM_RECEIPTS_UNREADABLE";
|
|
@@ -33,6 +33,13 @@ export interface ReclaimHooks {
|
|
|
33
33
|
/** Tests inject a fake @e2b/desktop module; default loads the real one. */
|
|
34
34
|
loadModule?: () => Promise<E2BDesktopModule>;
|
|
35
35
|
requestTimeoutMs?: number;
|
|
36
|
+
/** Ask E2B whether each sandbox still exists, and kill nothing. */
|
|
37
|
+
check?: boolean;
|
|
38
|
+
/** The signal handler's view of this run's creates: the ids they reported, and those still
|
|
39
|
+
* in flight, whose sandboxes are killed as their ids arrive. */
|
|
40
|
+
creates?: StoppedSandboxCreates;
|
|
41
|
+
/** How long to wait for in-flight creates before the tag search runs anyway. */
|
|
42
|
+
createsWaitMs?: number;
|
|
36
43
|
}
|
|
37
44
|
export declare function reclaimRunSandboxes(cwd: string, runInput: string, hooks?: ReclaimHooks): Promise<ReclaimResult>;
|
|
38
45
|
/**
|
|
@@ -47,4 +54,3 @@ export declare function reclaimPinnedRunSandboxes(cwd: string, runPaths: Prepare
|
|
|
47
54
|
* be running is left alone with the reason (see preflightReclaimDecision).
|
|
48
55
|
*/
|
|
49
56
|
export declare function reclaimPreflightSandboxes(cwd: string, hooks?: ReclaimHooks): Promise<ReclaimResult>;
|
|
50
|
-
export {};
|