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
package/dist/run/reclaim.js
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
|
-
// `humanish reclaim`:
|
|
2
|
-
//
|
|
3
|
-
// the
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// `humanish reclaim`: stop an interrupted run's sandboxes and report what happened to each. It
|
|
2
|
+
// finds them three ways: the exact ids the run journaled at create time (sandbox-receipts.ndjson),
|
|
3
|
+
// the ids a create in this process reported before its receipt landed (the signal handler only),
|
|
4
|
+
// and E2B's list of sandboxes tagged with the run's owner tags (sandboxOwnerTags), which covers a
|
|
5
|
+
// sandbox whose id never reached the run. The list is filtered server-side by the run's tags and
|
|
6
|
+
// each result is checked against every tag again, so it can only return this run's sandboxes; an
|
|
7
|
+
// account-wide operation once destroyed unrelated infrastructure. Kills run concurrently, and
|
|
8
|
+
// reclaim-receipt.json is written before the first kill and after each one, so an exit at any
|
|
9
|
+
// point leaves a record of every sandbox not yet confirmed gone. `--check` asks E2B the same
|
|
10
|
+
// questions and kills nothing.
|
|
6
11
|
import { E2B_DEBUG_ENV, e2bDebugMode, loadE2BDesktopModule, } from "../substrates/e2b/sdk.js";
|
|
7
|
-
import { containedPathAbsent, readContainedRegularFile,
|
|
12
|
+
import { containedPathAbsent, readContainedRegularFile, } from "./contained-output.js";
|
|
8
13
|
import { discardPreflightJournal, listPreflightJournals, preflightReclaimDecision, } from "./preflight-receipts.js";
|
|
9
|
-
import { resolveRunPath } from "./locate.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
+
import { readRunJsonIfExists, resolveRunPath } from "./locate.js";
|
|
15
|
+
import { RUN_BUNDLE_FILE } from "./bundle.js";
|
|
16
|
+
import { RUN_STATUS_FILE } from "./status.js";
|
|
17
|
+
import { isRecord } from "./type-guards.js";
|
|
18
|
+
import { parseSandboxOwners, parseSandboxReceipts, SANDBOX_RECEIPTS_ARTIFACT, } from "./sandbox-receipts.js";
|
|
19
|
+
import { sandboxOwnerTags, } from "./sandbox-creates.js";
|
|
20
|
+
import { earlierOutcomes, GONE, RECLAIM_RECEIPT_ARTIFACT, RECLAIM_RESULT_SCHEMA, receiptWriter, rootState, searchTagSets, settledWithin, targetSet, } from "./reclaim-outcomes.js";
|
|
21
|
+
import { redactText, toErrorMessage } from "../evidence/redaction.js";
|
|
14
22
|
import { runIdOf } from "./paths.js";
|
|
15
|
-
|
|
16
|
-
export
|
|
23
|
+
import { cli } from "../cli/invocation.js";
|
|
24
|
+
export { RECLAIM_RECEIPT_ARTIFACT };
|
|
17
25
|
export function reclaimRunSandboxes(cwd, runInput, hooks = {}) {
|
|
18
26
|
return reclaimRun(cwd, runInput, () => resolveRunPath(cwd, runInput), hooks);
|
|
19
27
|
}
|
|
@@ -24,61 +32,90 @@ export function reclaimRunSandboxes(cwd, runInput, hooks = {}) {
|
|
|
24
32
|
export function reclaimPinnedRunSandboxes(cwd, runPaths, hooks = {}) {
|
|
25
33
|
return reclaimRun(cwd, runIdOf(runPaths), () => Promise.resolve(runPaths), hooks);
|
|
26
34
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const base = {
|
|
35
|
+
function emptyResult(cwd, runId, hooks, warnings) {
|
|
36
|
+
return {
|
|
30
37
|
schema: RECLAIM_RESULT_SCHEMA,
|
|
38
|
+
ok: false,
|
|
39
|
+
state: "unknown",
|
|
40
|
+
mode: hooks.check === true ? "check" : "kill",
|
|
31
41
|
cwd,
|
|
32
|
-
runId
|
|
42
|
+
runId,
|
|
33
43
|
receiptCount: 0,
|
|
34
44
|
outcomes: [],
|
|
45
|
+
tagSearch: { status: "not-run", found: 0 },
|
|
46
|
+
createsInFlight: 0,
|
|
35
47
|
warnings,
|
|
36
48
|
};
|
|
49
|
+
}
|
|
50
|
+
async function reclaimRun(cwd, runInput, locate, hooks) {
|
|
51
|
+
const warnings = [];
|
|
52
|
+
const base = emptyResult(cwd, runInput, hooks, warnings);
|
|
37
53
|
if (e2bDebugMode())
|
|
38
|
-
return { ...base,
|
|
54
|
+
return { ...base, error: E2B_DEBUG_REFUSAL };
|
|
39
55
|
const runPaths = await locate();
|
|
40
56
|
if (!runPaths) {
|
|
41
57
|
return {
|
|
42
58
|
...base,
|
|
43
|
-
ok: false,
|
|
44
59
|
error: {
|
|
45
60
|
code: "HUMANISH_RECLAIM_RUN_NOT_FOUND",
|
|
46
|
-
message: `No run found for "${runInput}" (use
|
|
61
|
+
message: `No run found for "${runInput}" (use \`${cli("runs")}\` to list runs).`,
|
|
47
62
|
},
|
|
48
63
|
};
|
|
49
64
|
}
|
|
50
65
|
const runId = runIdOf(runPaths);
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// pretending a scan happened.
|
|
57
|
-
warnings.push("No sandbox-receipts.ndjson in this run dir: either no sandbox was created before the interrupt, or the run predates create-time receipts. Nothing to reclaim by id; server-side kill-on-timeout covers anything that did exist.");
|
|
58
|
-
return { ...base, runId, ok: true };
|
|
59
|
-
}
|
|
60
|
-
if (journal.kind === "module-unavailable") {
|
|
66
|
+
// The signal handler passes the creates it stopped and decides from what they reported.
|
|
67
|
+
if (hooks.creates === undefined && (await recordedDryRun(runPaths)))
|
|
68
|
+
return { ...base, ok: true, state: "clean", runId, reason: "dry-run" };
|
|
69
|
+
const reclaimed = await reclaimRoot(runPaths, runId, hooks, warnings);
|
|
70
|
+
if (reclaimed.kind === "module-unavailable")
|
|
61
71
|
return {
|
|
62
72
|
...base,
|
|
63
73
|
runId,
|
|
64
|
-
receiptCount:
|
|
65
|
-
|
|
66
|
-
error: { code: "HUMANISH_RECLAIM_MODULE_UNAVAILABLE", message: journal.message },
|
|
74
|
+
receiptCount: reclaimed.receiptCount,
|
|
75
|
+
error: { code: "HUMANISH_RECLAIM_MODULE_UNAVAILABLE", message: reclaimed.message },
|
|
67
76
|
};
|
|
68
|
-
|
|
69
|
-
if (journal.kind === "unreadable") {
|
|
77
|
+
if (reclaimed.kind === "unreadable")
|
|
70
78
|
return {
|
|
71
79
|
...base,
|
|
72
80
|
runId,
|
|
73
|
-
ok: false,
|
|
74
81
|
error: { code: "HUMANISH_RECLAIM_RECEIPTS_UNREADABLE", message: UNREADABLE_MESSAGE },
|
|
75
82
|
};
|
|
83
|
+
const { state, receiptCount, outcomes, tagSearch, createsInFlight } = reclaimed;
|
|
84
|
+
return {
|
|
85
|
+
...base,
|
|
86
|
+
ok: state === "clean",
|
|
87
|
+
state,
|
|
88
|
+
runId,
|
|
89
|
+
receiptCount,
|
|
90
|
+
outcomes,
|
|
91
|
+
tagSearch,
|
|
92
|
+
createsInFlight,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Whether the run records that it was a dry run: run.json and status.json, each one present, say
|
|
97
|
+
* `dry-run`. A run with neither record is not taken for one. A journal with any line in it, or an
|
|
98
|
+
* earlier reclaim receipt that names a sandbox, means a create ran, so that run is reclaimed as a
|
|
99
|
+
* live one whatever its records say.
|
|
100
|
+
*/
|
|
101
|
+
async function recordedDryRun(runPaths) {
|
|
102
|
+
const modes = [];
|
|
103
|
+
for (const file of [RUN_BUNDLE_FILE, RUN_STATUS_FILE]) {
|
|
104
|
+
const record = await readRunJsonIfExists(runPaths, file);
|
|
105
|
+
if (isRecord(record))
|
|
106
|
+
modes.push(record.mode);
|
|
107
|
+
}
|
|
108
|
+
if (modes.length === 0 || modes.some((mode) => mode !== "dry-run"))
|
|
109
|
+
return false;
|
|
110
|
+
if (!(await containedPathAbsent(runPaths, RECLAIM_RECEIPT_ARTIFACT))) {
|
|
111
|
+
const earlier = await readRunJsonIfExists(runPaths, RECLAIM_RECEIPT_ARTIFACT);
|
|
112
|
+
if (!isRecord(earlier) || !Array.isArray(earlier.outcomes) || earlier.outcomes.length > 0)
|
|
113
|
+
return false;
|
|
76
114
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return result;
|
|
115
|
+
if (await containedPathAbsent(runPaths, SANDBOX_RECEIPTS_ARTIFACT))
|
|
116
|
+
return true;
|
|
117
|
+
const journal = await readContainedRegularFile(runPaths, SANDBOX_RECEIPTS_ARTIFACT);
|
|
118
|
+
return journal !== null && journal.toString("utf8").trim() === "";
|
|
82
119
|
}
|
|
83
120
|
/**
|
|
84
121
|
* Kill the sandboxes left by interrupted `humanish study check` probes. A probe journals under
|
|
@@ -88,52 +125,71 @@ async function reclaimRun(cwd, runInput, locate, hooks) {
|
|
|
88
125
|
*/
|
|
89
126
|
export async function reclaimPreflightSandboxes(cwd, hooks = {}) {
|
|
90
127
|
const warnings = [];
|
|
128
|
+
const base = emptyResult(cwd, "preflight", hooks, warnings);
|
|
129
|
+
if (e2bDebugMode())
|
|
130
|
+
return { ...base, error: E2B_DEBUG_REFUSAL };
|
|
91
131
|
const outcomes = [];
|
|
92
132
|
let receiptCount = 0;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return { ...base, ok: false, receiptCount, outcomes, error: E2B_DEBUG_REFUSAL };
|
|
133
|
+
const states = [];
|
|
134
|
+
// Not run until a journal is reclaimed; then done unless one journal's search was not.
|
|
135
|
+
const tagSearch = { status: "not-run", found: 0 };
|
|
97
136
|
const journals = await listPreflightJournals(cwd);
|
|
98
137
|
if (journals.length === 0) {
|
|
99
|
-
warnings.push("No preflight journals in .humanish/preflight: every probe's teardown was confirmed, or no probe ran here. Nothing to reclaim
|
|
138
|
+
warnings.push("No preflight journals in .humanish/preflight: every probe's teardown was confirmed, or no probe ran here. Nothing to reclaim.");
|
|
100
139
|
}
|
|
101
140
|
for (const journal of journals) {
|
|
102
141
|
const decision = await preflightReclaimDecision(journal, Date.now());
|
|
103
142
|
if (!decision.reclaim) {
|
|
104
143
|
warnings.push(`Preflight ${journal.id} left alone: ${decision.reason}.`);
|
|
144
|
+
states.push("unknown");
|
|
105
145
|
continue;
|
|
106
146
|
}
|
|
107
|
-
const reclaimed = await
|
|
147
|
+
const reclaimed = await reclaimRoot(journal.root, journal.id, hooks, warnings);
|
|
108
148
|
if (reclaimed.kind === "unreadable") {
|
|
109
149
|
// The receipt may name a live sandbox; the journal stays for a later reclaim.
|
|
110
|
-
unreadable = true;
|
|
111
150
|
warnings.push(`Preflight ${journal.id} left alone: ${UNREADABLE_MESSAGE}`);
|
|
151
|
+
states.push("unknown");
|
|
112
152
|
continue;
|
|
113
153
|
}
|
|
114
154
|
if (reclaimed.kind === "module-unavailable") {
|
|
115
155
|
return {
|
|
116
156
|
...base,
|
|
117
|
-
ok: false,
|
|
118
157
|
receiptCount: receiptCount + reclaimed.receiptCount,
|
|
119
158
|
outcomes,
|
|
120
159
|
error: { code: "HUMANISH_RECLAIM_MODULE_UNAVAILABLE", message: reclaimed.message },
|
|
121
160
|
};
|
|
122
161
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
162
|
+
receiptCount += reclaimed.receiptCount;
|
|
163
|
+
outcomes.push(...reclaimed.outcomes);
|
|
164
|
+
const { state } = reclaimed;
|
|
165
|
+
states.push(state);
|
|
166
|
+
tagSearch.found += reclaimed.tagSearch.found;
|
|
167
|
+
if (tagSearch.status === "not-run" || reclaimed.tagSearch.status !== "done") {
|
|
168
|
+
tagSearch.status = reclaimed.tagSearch.status;
|
|
169
|
+
if (reclaimed.tagSearch.detail !== undefined)
|
|
170
|
+
tagSearch.detail = reclaimed.tagSearch.detail;
|
|
171
|
+
}
|
|
172
|
+
// The journal stays until its sandboxes are known to be gone, so a later reclaim can try
|
|
173
|
+
// again; its receipt says what happened. Elapsed time alone never removes it: a probe can
|
|
174
|
+
// create later than its journal says, and a journal from before owner lines cannot show that
|
|
175
|
+
// nothing it made is still running. A check never removes it.
|
|
176
|
+
if (state !== "clean" || hooks.check === true) {
|
|
177
|
+
if (state === "unknown" && reclaimed.exhausted && hooks.check !== true)
|
|
178
|
+
warnings.push(`Preflight ${journal.id} is kept: humanish cannot rule out a sandbox it made. Once the E2B dashboard shows no sandbox for it, delete .humanish/preflight/${journal.id}.`);
|
|
179
|
+
continue;
|
|
131
180
|
}
|
|
132
181
|
await discardPreflightJournal(journal, [RECLAIM_RECEIPT_ARTIFACT]).catch((error) => {
|
|
133
182
|
warnings.push(`Preflight ${journal.id} was reclaimed but its journal could not be removed: ${redactText(toErrorMessage(error))}`);
|
|
134
183
|
});
|
|
135
184
|
}
|
|
136
|
-
|
|
185
|
+
const state = combinedState(states);
|
|
186
|
+
return { ...base, ok: state === "clean", state, receiptCount, outcomes, tagSearch };
|
|
187
|
+
}
|
|
188
|
+
function combinedState(states) {
|
|
189
|
+
for (const worst of ["running", "unconfirmed", "unknown"])
|
|
190
|
+
if (states.includes(worst))
|
|
191
|
+
return worst;
|
|
192
|
+
return "clean";
|
|
137
193
|
}
|
|
138
194
|
// In debug mode the SDK returns true from Sandbox.kill without contacting E2B, so every receipt
|
|
139
195
|
// would read killed and preflight journals would be discarded for sandboxes that may still run.
|
|
@@ -142,69 +198,112 @@ const E2B_DEBUG_REFUSAL = {
|
|
|
142
198
|
message: `${E2B_DEBUG_ENV}=true puts the E2B SDK in debug mode, where Sandbox.kill reports success without contacting E2B. Nothing was killed and the receipts were kept; unset ${E2B_DEBUG_ENV} and run reclaim again.`,
|
|
143
199
|
};
|
|
144
200
|
const UNREADABLE_MESSAGE = "sandbox-receipts.ndjson is present but could not be read safely (not a single regular file, or a read error). Nothing was killed and the receipts were kept; check the file, then run reclaim again.";
|
|
145
|
-
/**
|
|
146
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Find and stop (or, for `--check`, look up) every sandbox one directory owns: its receipts, the
|
|
203
|
+
* creates this process reported, and E2B's tagged list, each once by exact id.
|
|
204
|
+
*/
|
|
205
|
+
async function reclaimRoot(root, label, hooks, warnings) {
|
|
147
206
|
const bytes = await readContainedRegularFile(root, SANDBOX_RECEIPTS_ARTIFACT);
|
|
148
|
-
if (bytes === null)
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
207
|
+
if (bytes === null && !(await containedPathAbsent(root, SANDBOX_RECEIPTS_ARTIFACT)))
|
|
208
|
+
return { kind: "unreadable" };
|
|
209
|
+
const journal = bytes === null ? "" : bytes.toString("utf8");
|
|
210
|
+
const receipts = parseSandboxReceipts(journal);
|
|
211
|
+
const owners = parseSandboxOwners(journal, label);
|
|
212
|
+
const check = hooks.check === true;
|
|
213
|
+
const requestTimeoutMs = hooks.requestTimeoutMs ?? 60_000;
|
|
214
|
+
// The SDK lists by tag as well as killing by id, so it is needed even with no receipt. Without
|
|
215
|
+
// it, an E2B receipt cannot be acted on; with no E2B receipt, only the tag search is lost.
|
|
154
216
|
let e2b;
|
|
217
|
+
let moduleError;
|
|
155
218
|
try {
|
|
156
|
-
|
|
157
|
-
e2b = await (hooks.loadModule ?? loadE2BDesktopModule)();
|
|
219
|
+
e2b = await (hooks.loadModule ?? loadE2BDesktopModule)();
|
|
158
220
|
}
|
|
159
221
|
catch (error) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
receiptCount: receipts.length,
|
|
163
|
-
message: `Cannot load @e2b/desktop to kill by id: ${redactText(toErrorMessage(error))}`,
|
|
164
|
-
};
|
|
222
|
+
moduleError = `Cannot load @e2b/desktop to reach E2B: ${redactText(toErrorMessage(error))}`;
|
|
223
|
+
if (receipts.some((receipt) => receipt.provider === "e2b") || hooks.creates !== undefined)
|
|
224
|
+
return { kind: "module-unavailable", receiptCount: receipts.length, message: moduleError };
|
|
165
225
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
226
|
+
// An earlier reclaim may have killed a sandbox no receipt names (the signal handler's creates
|
|
227
|
+
// and tag finds). Its outcome is carried forward, so a later reclaim never erases that record.
|
|
228
|
+
const earlier = check ? [] : await earlierOutcomes(root, label);
|
|
229
|
+
const set = targetSet({
|
|
230
|
+
e2b,
|
|
231
|
+
check,
|
|
232
|
+
requestTimeoutMs,
|
|
233
|
+
earlier,
|
|
234
|
+
persist: receiptWriter(root, label, receipts.length, check, warnings),
|
|
235
|
+
});
|
|
236
|
+
// The watch replays the creates reported so far, synchronously, then each one an in-flight
|
|
237
|
+
// create reports, until reclaim returns. The replayed ones are matched to receipts first, so a
|
|
238
|
+
// sandbox the route already released is skipped whichever record names it.
|
|
239
|
+
const replayed = new Map();
|
|
240
|
+
let replaying = true;
|
|
241
|
+
const unwatch = hooks.creates?.watch((sandbox) => {
|
|
242
|
+
if (replaying)
|
|
243
|
+
replayed.set(sandbox.sandboxId, sandbox);
|
|
244
|
+
else
|
|
245
|
+
set.add("e2b", sandbox.sandboxId, sandbox.participantId, "create", sandbox.released);
|
|
246
|
+
});
|
|
247
|
+
replaying = false;
|
|
180
248
|
for (const receipt of receipts) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (seen.has(key))
|
|
184
|
-
continue;
|
|
185
|
-
seen.add(key);
|
|
186
|
-
// The result and reclaim-receipt.json name the sandbox by digest, and a provider error that
|
|
187
|
-
// quotes the id is scrubbed; the raw id stays in sandbox-receipts.ndjson.
|
|
188
|
-
const attempt = await destroy(receipt);
|
|
189
|
-
outcomes.push({
|
|
190
|
-
sandboxId: REDACTED_SANDBOX_ID,
|
|
191
|
-
sandboxIdDigest: sandboxIdDigest(receipt.sandboxId),
|
|
192
|
-
laneId: receipt.laneId,
|
|
193
|
-
...("detail" in attempt
|
|
194
|
-
? { ...attempt, detail: scrubSandboxIds(attempt.detail, [receipt.sandboxId]) }
|
|
195
|
-
: attempt),
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
return { kind: "done", receiptCount: receipts.length, outcomes };
|
|
199
|
-
}
|
|
200
|
-
function allGone(outcomes) {
|
|
201
|
-
return outcomes.every((outcome) => outcome.state === "killed" || outcome.state === "already-gone");
|
|
202
|
-
}
|
|
203
|
-
async function writeReclaimReceipt(root, runId, receiptCount, outcomes, warnings) {
|
|
204
|
-
try {
|
|
205
|
-
await writeContainedOutputFile(root, RECLAIM_RECEIPT_ARTIFACT, `${JSON.stringify({ schema: RECLAIM_RESULT_SCHEMA, at: new Date().toISOString(), runId, receiptCount, outcomes }, null, 2)}\n`, "utf8");
|
|
249
|
+
const released = receipt.provider === "e2b" && replayed.get(receipt.sandboxId)?.released;
|
|
250
|
+
set.add(receipt.provider, receipt.sandboxId, receipt.laneId, "receipt", released === true);
|
|
206
251
|
}
|
|
207
|
-
|
|
208
|
-
|
|
252
|
+
for (const sandbox of replayed.values())
|
|
253
|
+
set.add("e2b", sandbox.sandboxId, sandbox.participantId, "create", sandbox.released);
|
|
254
|
+
await set.start();
|
|
255
|
+
if (hooks.creates !== undefined)
|
|
256
|
+
await settledWithin(hooks.creates.settled, hooks.createsWaitMs);
|
|
257
|
+
// The tags the run recorded before each create, and this directory's own: a copied or moved
|
|
258
|
+
// run directory computes other tags than the ones its sandboxes carry.
|
|
259
|
+
const search = await searchTagSets(e2b, [...owners, sandboxOwnerTags(root)], {
|
|
260
|
+
requestTimeoutMs,
|
|
261
|
+
unavailable: moduleError ?? "no E2B SDK",
|
|
262
|
+
});
|
|
263
|
+
let found = 0;
|
|
264
|
+
for (const listed of search.sandboxes) {
|
|
265
|
+
const metadata = listed.metadata ?? {};
|
|
266
|
+
const participant = metadata.participantId ?? metadata.kind ?? "unknown";
|
|
267
|
+
if (set.add("e2b", listed.sandboxId, participant, "tag"))
|
|
268
|
+
found += 1;
|
|
209
269
|
}
|
|
270
|
+
const tagSearch = {
|
|
271
|
+
status: search.status,
|
|
272
|
+
found,
|
|
273
|
+
...(search.detail === undefined ? {} : { detail: search.detail }),
|
|
274
|
+
};
|
|
275
|
+
if (search.status !== "done")
|
|
276
|
+
warnings.push(`Could not search E2B for sandboxes tagged with ${label}: ${search.detail ?? search.status}. A sandbox whose id never reached a receipt would not be found.`);
|
|
277
|
+
// A create that reports its id while the search or a kill is still running is killed too.
|
|
278
|
+
await set.drain();
|
|
279
|
+
unwatch?.();
|
|
280
|
+
const createsInFlight = hooks.creates?.inFlight() ?? 0;
|
|
281
|
+
// An empty search proves nothing for a run that never recorded its tags: one from humanish
|
|
282
|
+
// 0.110.0 or earlier, or whose owner lines were lost. The process that made the creates knows.
|
|
283
|
+
const provenance = owners.length > 0 || hooks.creates !== undefined;
|
|
284
|
+
if (!provenance)
|
|
285
|
+
warnings.push(`${label} records no owner tags, so a sandbox whose id never reached a receipt cannot be ruled out: the run is from humanish 0.110.0 or earlier, it created no sandbox, or its journal lost those lines. Each sandbox's create-time timeout is the backstop.`);
|
|
286
|
+
// A carried sandbox from a create or a tag search carries this run's tags; if the finished
|
|
287
|
+
// search, with the tags the run recorded, no longer lists it, it is gone. One from an older
|
|
288
|
+
// receipt, or from a run with no recorded tags, is left as it was recorded.
|
|
289
|
+
const covered = provenance && tagSearch.status === "done";
|
|
290
|
+
const outcomes = set.snapshot(earlier.map((outcome) => GONE.has(outcome.state) || outcome.source === "receipt" || !covered
|
|
291
|
+
? outcome
|
|
292
|
+
: {
|
|
293
|
+
...outcome,
|
|
294
|
+
state: "already-gone",
|
|
295
|
+
detail: "an earlier reclaim left it unconfirmed, and E2B no longer lists it with this run's tags",
|
|
296
|
+
}));
|
|
297
|
+
const searched = tagSearch.status === "done" && createsInFlight === 0;
|
|
298
|
+
const state = rootState(outcomes, check, searched && provenance);
|
|
299
|
+
await set.finish(outcomes, state);
|
|
300
|
+
return {
|
|
301
|
+
kind: "done",
|
|
302
|
+
state,
|
|
303
|
+
exhausted: searched && outcomes.every((outcome) => GONE.has(outcome.state)),
|
|
304
|
+
receiptCount: receipts.length,
|
|
305
|
+
outcomes,
|
|
306
|
+
tagSearch,
|
|
307
|
+
createsInFlight,
|
|
308
|
+
};
|
|
210
309
|
}
|
package/dist/run/results.d.ts
CHANGED
|
@@ -34,27 +34,26 @@ export interface RunResult {
|
|
|
34
34
|
message: string;
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* A resource line of cleanup.json. `humanish cleanup` wrote the file through 0.110.0, and verify
|
|
39
|
+
* still reads one a run kept. v0.12.23 through v0.15.0 killed sandboxes and wrote `killed`.
|
|
40
|
+
*/
|
|
41
|
+
export interface StoredCleanupResourceResult {
|
|
38
42
|
provider: RunProviderResource["provider"];
|
|
39
43
|
kind: RunProviderResource["kind"];
|
|
40
|
-
/** "[redacted-sandbox-id]"
|
|
44
|
+
/** "[redacted-sandbox-id]" from 0.110; earlier files hold the raw id. */
|
|
41
45
|
id: string;
|
|
42
|
-
/** The id's digest, which matches its receipt; absent when the record had neither. */
|
|
43
46
|
idDigest?: string;
|
|
44
|
-
|
|
45
|
-
status: "already_clean" | "failed" | "skipped";
|
|
47
|
+
status: "already_clean" | "failed" | "skipped" | "killed";
|
|
46
48
|
message: string;
|
|
47
49
|
}
|
|
48
|
-
/** A resource line as a stored cleanup.json holds it. v0.12.23 through v0.15.0 killed sandboxes. */
|
|
49
|
-
export interface StoredCleanupResourceResult extends Omit<CleanupResourceResult, "status"> {
|
|
50
|
-
status: CleanupResourceResult["status"] | "killed";
|
|
51
|
-
}
|
|
52
50
|
export interface CleanupAdapterResult {
|
|
53
51
|
id: string;
|
|
54
52
|
ok: boolean;
|
|
55
53
|
message: string;
|
|
56
54
|
}
|
|
57
|
-
|
|
55
|
+
/** cleanup.json as any release through 0.110.0 wrote it. */
|
|
56
|
+
export interface StoredCleanupResult {
|
|
58
57
|
schema: typeof CLEANUP_SCHEMA;
|
|
59
58
|
ok: boolean;
|
|
60
59
|
cwd: string;
|
|
@@ -65,23 +64,14 @@ export interface CleanupResult {
|
|
|
65
64
|
checkedAt: string;
|
|
66
65
|
summary: {
|
|
67
66
|
resources: number;
|
|
68
|
-
/** Always 0 now; written so readers from earlier releases still accept the file. */
|
|
69
67
|
killed: number;
|
|
70
68
|
alreadyClean: number;
|
|
71
69
|
failed: number;
|
|
72
70
|
skipped: number;
|
|
73
71
|
};
|
|
74
|
-
resources:
|
|
72
|
+
resources: StoredCleanupResourceResult[];
|
|
75
73
|
adapterResults: CleanupAdapterResult[];
|
|
76
74
|
warnings: string[];
|
|
77
|
-
error?: {
|
|
78
|
-
code: "HUMANISH_RUN_NOT_FOUND" | "HUMANISH_INVALID_RUN_BUNDLE";
|
|
79
|
-
message: string;
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
/** cleanup.json as any release wrote it; see StoredCleanupResourceResult. */
|
|
83
|
-
export interface StoredCleanupResult extends Omit<CleanupResult, "resources"> {
|
|
84
|
-
resources: StoredCleanupResourceResult[];
|
|
85
75
|
}
|
|
86
76
|
export interface RunPointer {
|
|
87
77
|
schema: "humanish.latest-run.v1";
|
package/dist/run/results.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
// The run command's options and result, the cleanup
|
|
2
|
-
// pointer (latest.json). None of them are stored in run.json.
|
|
1
|
+
// The run command's options and result, the cleanup.json shape releases through 0.110.0 wrote, and
|
|
2
|
+
// the latest-run pointer (latest.json). None of them are stored in run.json.
|
|
3
3
|
export const CLEANUP_SCHEMA = "humanish.cleanup-result.v1";
|
package/dist/run/run-index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { AnalysisCostCache } from "./run-index-analysis.js";
|
|
|
2
2
|
import type { RunAnalysisCost } from "./run-cost.js";
|
|
3
3
|
import { type RunLiveness, type RunStatusRecord } from "./status.js";
|
|
4
4
|
import { type RunStudyProvenance } from "./study-provenance.js";
|
|
5
|
+
import type { ExecutionFailure } from "./judge.js";
|
|
5
6
|
declare const RUN_INDEX_SCHEMA = "humanish.run-index.v1";
|
|
6
7
|
export interface RunIndexEntry {
|
|
7
8
|
runId: string;
|
|
@@ -20,6 +21,14 @@ export interface RunIndexEntry {
|
|
|
20
21
|
updatedAt?: string;
|
|
21
22
|
completedAt?: string;
|
|
22
23
|
verdict?: string;
|
|
24
|
+
/**
|
|
25
|
+
* The run's own ok, from run.json's outcome as status.json copies it, or from the status record
|
|
26
|
+
* alone for a run recorded before run.json carried one. Absent when the run recorded none.
|
|
27
|
+
* runDisplay (src/run/display.ts) reads it with `verdict` and `liveness`.
|
|
28
|
+
*/
|
|
29
|
+
ok?: boolean;
|
|
30
|
+
/** The first execution failure that failed the run, beside `ok`. */
|
|
31
|
+
failure?: ExecutionFailure;
|
|
23
32
|
participants?: {
|
|
24
33
|
total: number;
|
|
25
34
|
reachedGoal: number;
|
|
@@ -92,13 +101,14 @@ interface BundleFacts {
|
|
|
92
101
|
estimatedTotalUsd?: number | null;
|
|
93
102
|
fullyEstimated?: unknown;
|
|
94
103
|
};
|
|
104
|
+
outcome?: unknown;
|
|
95
105
|
}
|
|
96
106
|
/**
|
|
97
107
|
* One run's liveness from files already read: its status record when usable, else its bundle.
|
|
98
108
|
* `readRunIndex` reads in the same order; `humanish verify` calls this with both in hand. `record`
|
|
99
109
|
* is the status record the liveness came from, when there was one.
|
|
100
110
|
*/
|
|
101
|
-
export declare function runLiveness(runId: string, status: unknown, bundle: Pick<BundleFacts, "simulations">, nowMs: number): {
|
|
111
|
+
export declare function runLiveness(runId: string, status: unknown, bundle: Pick<BundleFacts, "simulations" | "outcome">, nowMs: number): {
|
|
102
112
|
liveness: RunLiveness;
|
|
103
113
|
record?: RunStatusRecord;
|
|
104
114
|
};
|
package/dist/run/run-index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { AnalysisCostCache, readIndexedAnalysisCost } from "./run-index-analysis
|
|
|
19
19
|
import { RUN_STATUS_FILE, classifyRunStatus, isRunStatusRecord, } from "./status.js";
|
|
20
20
|
import { studyProvenanceOf } from "./study-provenance.js";
|
|
21
21
|
import { RUN_BUNDLE_FILE } from "./bundle.js";
|
|
22
|
+
import { isRunOutcome } from "./bundle-shape.js";
|
|
22
23
|
const RUN_INDEX_SCHEMA = "humanish.run-index.v1";
|
|
23
24
|
/**
|
|
24
25
|
* A process-lifetime cache. Deliberately explicit rather than module-global state: a caller that
|
|
@@ -88,6 +89,10 @@ function entryFromStatus(record, nowMs) {
|
|
|
88
89
|
updatedAt: record.updatedAt,
|
|
89
90
|
...(record.completedAt === undefined ? {} : { completedAt: record.completedAt }),
|
|
90
91
|
...(record.outcome?.verdict === undefined ? {} : { verdict: record.outcome.verdict }),
|
|
92
|
+
...(typeof record.outcome?.ok === "boolean" ? { ok: record.outcome.ok } : {}),
|
|
93
|
+
...(record.outcome?.execution?.failures[0] === undefined
|
|
94
|
+
? {}
|
|
95
|
+
: { failure: record.outcome.execution.failures[0] }),
|
|
91
96
|
...(record.outcome?.participants === undefined
|
|
92
97
|
? {}
|
|
93
98
|
: { participants: record.outcome.participants }),
|
|
@@ -112,6 +117,8 @@ function usableStatusRecord(raw, runId) {
|
|
|
112
117
|
* reading of "it started, and nothing here says it finished" is interrupted, not finished.
|
|
113
118
|
*/
|
|
114
119
|
function bundleLiveness(bundle) {
|
|
120
|
+
if (isRunOutcome(bundle.outcome))
|
|
121
|
+
return bundle.outcome.state;
|
|
115
122
|
const inProgress = (bundle.simulations ?? []).some((simulation) => simulation?.status === "running");
|
|
116
123
|
return inProgress ? "interrupted" : "finished";
|
|
117
124
|
}
|
|
@@ -129,6 +136,8 @@ function studyEntry(study) {
|
|
|
129
136
|
return study === undefined ? {} : { study };
|
|
130
137
|
}
|
|
131
138
|
function entryFromBundle(runId, bundle) {
|
|
139
|
+
const outcome = isRunOutcome(bundle.outcome) ? bundle.outcome : undefined;
|
|
140
|
+
const failure = outcome?.state === "finished" ? outcome.execution.failures[0] : undefined;
|
|
132
141
|
return {
|
|
133
142
|
runId,
|
|
134
143
|
derivedFrom: "bundle",
|
|
@@ -137,6 +146,8 @@ function entryFromBundle(runId, bundle) {
|
|
|
137
146
|
...studyEntry(studyProvenanceOf(bundle)),
|
|
138
147
|
...(bundle.createdAt === undefined ? {} : { startedAt: bundle.createdAt }),
|
|
139
148
|
...(bundle.review?.verdict === undefined ? {} : { verdict: bundle.review.verdict }),
|
|
149
|
+
...(outcome === undefined ? {} : { ok: outcome.ok }),
|
|
150
|
+
...(failure === undefined ? {} : { failure }),
|
|
140
151
|
...(bundle.review?.participants === undefined
|
|
141
152
|
? {}
|
|
142
153
|
: { participants: bundle.review.participants }),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { resolveRunsRoot } from "./paths.js";
|
|
4
|
+
import { cli } from "../cli/invocation.js";
|
|
4
5
|
/**
|
|
5
6
|
* The message for a run that is not there: how to start one when the project has no runs, or how
|
|
6
7
|
* to list them when it has. Only the runs directory's entry names are read.
|
|
@@ -10,6 +11,6 @@ export async function runNotFoundMessage(cwdInput, runInput) {
|
|
|
10
11
|
const entries = await readdir(resolveRunsRoot(cwd)).catch(() => []);
|
|
11
12
|
const hasRun = entries.some((name) => !name.startsWith(".") && !name.endsWith(".json"));
|
|
12
13
|
return hasRun
|
|
13
|
-
? `No run ${runInput};
|
|
14
|
-
: `No runs in ${cwd} yet; start one with
|
|
14
|
+
? `No run ${runInput}; ${cli("runs")} lists them.`
|
|
15
|
+
: `No runs in ${cwd} yet; start one with ${cli("run first-run")}.`;
|
|
15
16
|
}
|