humanish 0.110.0 → 0.110.1
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 +14 -11
- package/CHANGELOG.md +13 -0
- package/CONTEXT.md +8 -6
- package/README.md +96 -47
- 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 +11 -0
- package/dist/actors/local-agent/cli.js +103 -24
- package/dist/actors/local-agent/readiness.d.ts +2 -2
- package/dist/actors/local-agent/readiness.js +18 -11
- package/dist/cli/commands/analyze.js +21 -5
- package/dist/cli/commands/run-signals.d.ts +2 -0
- package/dist/cli/commands/run-signals.js +96 -44
- package/dist/cli/commands/runs.d.ts +5 -0
- package/dist/cli/commands/runs.js +86 -70
- package/dist/cli/commands/study-route-computer-use.js +2 -0
- package/dist/cli/commands/study-route-scripted.js +2 -0
- package/dist/cli/commands/study-route-shared-world.js +2 -0
- package/dist/cli/commands/study-route-terminal.js +2 -0
- package/dist/cli/doctor.d.ts +4 -1
- package/dist/cli/doctor.js +42 -4
- package/dist/cli/findings.d.ts +103 -0
- package/dist/cli/findings.js +316 -0
- package/dist/cli/first-run-path.d.ts +5 -4
- package/dist/cli/first-run-path.js +14 -7
- package/dist/cli/io.js +2 -1
- package/dist/cli/product-sentence.d.ts +3 -3
- package/dist/cli/product-sentence.js +3 -3
- package/dist/keys/env-file.d.ts +4 -0
- package/dist/keys/env-file.js +17 -0
- package/dist/observer/data.d.ts +14 -1
- package/dist/observer/data.js +19 -9
- package/dist/observer/library.d.ts +4 -0
- package/dist/observer/library.js +18 -15
- package/dist/observer/render.js +8 -2
- package/dist/observer/run-routes.d.ts +2 -0
- package/dist/observer/run-routes.js +43 -34
- 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 +4 -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 +1 -1
- package/dist/routes/shared-world/result.js +22 -22
- package/dist/routes/terminal/bundle.js +4 -2
- 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 +4 -4
- package/dist/routes/terminal/result.d.ts +6 -7
- package/dist/routes/terminal/result.js +6 -22
- 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 +11 -7
- package/dist/run/judge.d.ts +3 -2
- package/dist/run/judge.js +10 -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 +18 -17
- package/dist/run/reclaim.js +180 -113
- 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.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 +138 -0
- package/dist/run/sandbox-receipts.d.ts +14 -0
- package/dist/run/sandbox-receipts.js +44 -0
- 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/doctor.d.ts +1 -1
- package/dist/study/doctor.js +5 -2
- package/dist/study/init.js +14 -3
- package/dist/study/keys.d.ts +12 -0
- package/dist/study/keys.js +32 -0
- package/dist/study/persona.d.ts +4 -0
- package/dist/study/persona.js +4 -2
- 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 +128 -35
- package/dist/substrates/e2b/sdk.d.ts +33 -0
- package/dist/substrates/e2b/sdk.js +21 -21
- package/dist/substrates/shell.d.ts +2 -0
- package/dist/substrates/shell.js +4 -0
- package/dist/tui-app.js +133 -133
- package/dist/verify/liveness.d.ts +19 -3
- package/dist/verify/liveness.js +46 -25
- package/dist/verify/verify.d.ts +3 -0
- package/dist/verify/verify.js +6 -2
- 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 +1 -1
- package/docs/contracts/run-bundle.md +75 -6
- package/docs/contracts/schemas.md +39 -22
- 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 +4 -3
- package/skills/humanish/SKILL.md +16 -2
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,23 @@
|
|
|
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;
|
|
26
21
|
warnings: string[];
|
|
27
22
|
error?: {
|
|
28
23
|
code: "HUMANISH_RECLAIM_RUN_NOT_FOUND" | "HUMANISH_RECLAIM_E2B_DEBUG" | "HUMANISH_RECLAIM_MODULE_UNAVAILABLE" | "HUMANISH_RECLAIM_RECEIPTS_UNREADABLE";
|
|
@@ -33,6 +28,13 @@ export interface ReclaimHooks {
|
|
|
33
28
|
/** Tests inject a fake @e2b/desktop module; default loads the real one. */
|
|
34
29
|
loadModule?: () => Promise<E2BDesktopModule>;
|
|
35
30
|
requestTimeoutMs?: number;
|
|
31
|
+
/** Ask E2B whether each sandbox still exists, and kill nothing. */
|
|
32
|
+
check?: boolean;
|
|
33
|
+
/** The signal handler's view of this run's creates: the ids they reported, and those still
|
|
34
|
+
* in flight, whose sandboxes are killed as their ids arrive. */
|
|
35
|
+
creates?: StoppedSandboxCreates;
|
|
36
|
+
/** How long to wait for in-flight creates before the tag search runs anyway. */
|
|
37
|
+
createsWaitMs?: number;
|
|
36
38
|
}
|
|
37
39
|
export declare function reclaimRunSandboxes(cwd: string, runInput: string, hooks?: ReclaimHooks): Promise<ReclaimResult>;
|
|
38
40
|
/**
|
|
@@ -47,4 +49,3 @@ export declare function reclaimPinnedRunSandboxes(cwd: string, runPaths: Prepare
|
|
|
47
49
|
* be running is left alone with the reason (see preflightReclaimDecision).
|
|
48
50
|
*/
|
|
49
51
|
export declare function reclaimPreflightSandboxes(cwd: string, hooks?: ReclaimHooks): Promise<ReclaimResult>;
|
|
50
|
-
export {};
|