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
|
@@ -21,13 +21,15 @@
|
|
|
21
21
|
// operator's machine. The same trick on the terminal route would be the opposite: it would move
|
|
22
22
|
// code execution out of the sandbox and onto a real disk, which is why this is a computer-use
|
|
23
23
|
// provider and nothing else. Even so, these are coding agents with their own shell and file tools,
|
|
24
|
-
// so each one is spawned tool-restricted, in a scratch directory, with a per-turn timeout.
|
|
24
|
+
// so each one is spawned tool-restricted, in a scratch directory, with a per-turn timeout. Claude
|
|
25
|
+
// Code's flags, environment and stream checks are in claude-participant.ts.
|
|
25
26
|
import { spawn } from "node:child_process";
|
|
26
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
27
|
+
import { mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
|
|
27
28
|
import { tmpdir } from "node:os";
|
|
28
29
|
import path from "node:path";
|
|
29
30
|
import { admitsCodexCliVersion, parseCodexCliVersion } from "../codex/codex-admission.js";
|
|
30
31
|
import { restrictedCodexNpmTarget } from "../codex/restricted-executable.js";
|
|
32
|
+
import { ClaudeStreamGuard, admitsClaudeCodeVersion, claudeCodeVersionMessage, claudeParticipantEnv, claudeParticipantFlags, parseClaudeCodeVersion, } from "./claude-participant.js";
|
|
31
33
|
const LOCAL_AGENTS = [
|
|
32
34
|
{ id: "codex", bin: "codex", label: "Codex", credentialPath: ".codex/auth.json" },
|
|
33
35
|
{
|
|
@@ -120,9 +122,14 @@ export function parseAgentJson(text) {
|
|
|
120
122
|
throw new Error("the local agent did not return a JSON object");
|
|
121
123
|
}
|
|
122
124
|
const defaultSpawn = async (bin, args, options) => await new Promise((resolve) => {
|
|
123
|
-
const child = spawn(bin, [...args], {
|
|
125
|
+
const child = spawn(bin, [...args], {
|
|
126
|
+
cwd: options.cwd,
|
|
127
|
+
env: options.env,
|
|
128
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
129
|
+
});
|
|
124
130
|
let stdout = "";
|
|
125
131
|
let stderr = "";
|
|
132
|
+
let scanned = 0;
|
|
126
133
|
const timer = setTimeout(() => child.kill("SIGKILL"), options.timeoutMs);
|
|
127
134
|
// Stopping a run must stop the thinking too: a local agent mid-turn can hold a terminal for
|
|
128
135
|
// minutes, and a Stop that leaves it running is not a stop.
|
|
@@ -136,6 +143,12 @@ const defaultSpawn = async (bin, args, options) => await new Promise((resolve) =
|
|
|
136
143
|
};
|
|
137
144
|
child.stdout?.on("data", (chunk) => {
|
|
138
145
|
stdout += chunk.toString("utf8");
|
|
146
|
+
for (let end = stdout.indexOf("\n", scanned); end >= 0; end = stdout.indexOf("\n", scanned)) {
|
|
147
|
+
const line = stdout.slice(scanned, end);
|
|
148
|
+
scanned = end + 1;
|
|
149
|
+
if (options.onLine?.(line) === true)
|
|
150
|
+
child.kill("SIGKILL");
|
|
151
|
+
}
|
|
139
152
|
});
|
|
140
153
|
child.stderr?.on("data", (chunk) => {
|
|
141
154
|
stderr += chunk.toString("utf8");
|
|
@@ -214,35 +227,33 @@ export function createLocalAgentProvider(options) {
|
|
|
214
227
|
const screenshotPath = path.join(work, "screen.png");
|
|
215
228
|
await writeFile(screenshotPath, frame);
|
|
216
229
|
const prompt = promptFor(request, screenshotPath);
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
"json",
|
|
221
|
-
// Read is the only tool it needs (the screenshot) and the only one it gets.
|
|
222
|
-
"--allowedTools",
|
|
223
|
-
"Read",
|
|
224
|
-
...(options.model === undefined ? [] : ["--model", options.model]),
|
|
225
|
-
// `--allowedTools` takes a list, so a prompt placed right after it is read as a tool
|
|
226
|
-
// name and Claude Code exits 1 with "Input must be provided". Three of three one-shot
|
|
227
|
-
// runs failed on turn one that way on 2026-09-01 (Claude Code 2.1.257); `--` ends the
|
|
228
|
-
// options so the prompt is the prompt.
|
|
229
|
-
"--",
|
|
230
|
-
prompt,
|
|
231
|
-
];
|
|
232
|
-
const result = await spawnFn(descriptor.bin, args, {
|
|
230
|
+
const folders = [work, await realpath(work).catch(() => work)];
|
|
231
|
+
const watch = new ClaudeStreamGuard(folders);
|
|
232
|
+
const result = await spawnFn(descriptor.bin, claudeOneShotArgs(prompt, options.model), {
|
|
233
233
|
cwd: work,
|
|
234
|
+
env: claudeParticipantEnv(options.env ?? process.env),
|
|
234
235
|
timeoutMs,
|
|
235
236
|
...(signal === undefined ? {} : { signal }),
|
|
237
|
+
// Stops the process as soon as a line is refused; the full scan below decides the turn.
|
|
238
|
+
onLine: (line) => inspectLine(watch, line) !== undefined,
|
|
236
239
|
});
|
|
240
|
+
const { refusal, result: final } = scanClaudeStream(result.stdout, folders);
|
|
241
|
+
if (refusal !== undefined)
|
|
242
|
+
throw refusal;
|
|
237
243
|
if (result.code !== 0) {
|
|
238
244
|
// Fail loud with the CLI's own words. A rate-limited plan says so here, and that is a
|
|
239
245
|
// sentence the operator can act on, unlike "turn failed".
|
|
240
246
|
const detail = (result.stderr || result.stdout).trim().slice(-400);
|
|
241
247
|
throw new Error(`${descriptor.label} exited ${result.code ?? "on a signal"}: ${detail}`);
|
|
242
248
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
249
|
+
if (final === undefined)
|
|
250
|
+
throw new Error(`${descriptor.label} returned no result message`);
|
|
251
|
+
if (final.is_error === true) {
|
|
252
|
+
const ending = typeof final.subtype === "string" ? final.subtype : "in error";
|
|
253
|
+
const said = typeof final.result === "string" ? final.result.slice(0, 160) : "";
|
|
254
|
+
throw new Error(`${descriptor.label} turn ended ${ending}: ${said}`);
|
|
255
|
+
}
|
|
256
|
+
const payload = typeof final.result === "string" ? final.result : "";
|
|
246
257
|
const turn = parseAgentJson(payload);
|
|
247
258
|
const actions = toCuaActions(Array.isArray(turn.actions) ? turn.actions : []);
|
|
248
259
|
const done = turn.done === true || (actions.length === 0 && typeof turn.message === "string");
|
|
@@ -268,6 +279,53 @@ export function createLocalAgentProvider(options) {
|
|
|
268
279
|
},
|
|
269
280
|
};
|
|
270
281
|
}
|
|
282
|
+
/** The one-shot argv: stream-json out so every message can be checked, then the prompt after `--`. */
|
|
283
|
+
function claudeOneShotArgs(prompt, model) {
|
|
284
|
+
return [
|
|
285
|
+
"-p",
|
|
286
|
+
"--output-format",
|
|
287
|
+
"stream-json",
|
|
288
|
+
// Required for stream-json output in -p mode.
|
|
289
|
+
"--verbose",
|
|
290
|
+
...claudeParticipantFlags(model),
|
|
291
|
+
// `--tools` takes a list, so a prompt placed right after it would be read as a tool name.
|
|
292
|
+
// `--` ends the options so the prompt is the prompt.
|
|
293
|
+
"--",
|
|
294
|
+
prompt,
|
|
295
|
+
];
|
|
296
|
+
}
|
|
297
|
+
/** The refusal one stdout line proves, if any. A line that is not JSON proves nothing. */
|
|
298
|
+
function inspectLine(guard, line) {
|
|
299
|
+
let message;
|
|
300
|
+
try {
|
|
301
|
+
message = JSON.parse(line);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
return message !== null && typeof message === "object" && !Array.isArray(message)
|
|
307
|
+
? guard.inspect(message)
|
|
308
|
+
: undefined;
|
|
309
|
+
}
|
|
310
|
+
/** The first refusal in a finished one-shot stream, and its last `result` message. */
|
|
311
|
+
function scanClaudeStream(stdout, folders) {
|
|
312
|
+
const guard = new ClaudeStreamGuard(folders);
|
|
313
|
+
let result;
|
|
314
|
+
for (const line of stdout.split("\n")) {
|
|
315
|
+
const refusal = inspectLine(guard, line);
|
|
316
|
+
if (refusal !== undefined)
|
|
317
|
+
return { refusal };
|
|
318
|
+
try {
|
|
319
|
+
const message = JSON.parse(line);
|
|
320
|
+
if (message?.type === "result")
|
|
321
|
+
result = message;
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// not a message
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return result === undefined ? {} : { result };
|
|
328
|
+
}
|
|
271
329
|
/** No shell, prompts or model request. Bound time and output even for a broken CLI. */
|
|
272
330
|
async function authProbe(bin, args, env) {
|
|
273
331
|
return await new Promise((resolve) => {
|
|
@@ -375,7 +433,10 @@ export async function detectLocalAgents(options = {}) {
|
|
|
375
433
|
.catch(() => false);
|
|
376
434
|
});
|
|
377
435
|
const found = [];
|
|
436
|
+
const probe = options.authProbe ?? authProbe;
|
|
378
437
|
for (const descriptor of LOCAL_AGENTS) {
|
|
438
|
+
if (options.only !== undefined && descriptor.id !== options.only)
|
|
439
|
+
continue;
|
|
379
440
|
const binPath = await which(descriptor.bin);
|
|
380
441
|
if (binPath === undefined)
|
|
381
442
|
continue;
|
|
@@ -384,24 +445,50 @@ export async function detectLocalAgents(options = {}) {
|
|
|
384
445
|
: descriptor.id === "claude" && env.CLAUDE_CONFIG_DIR
|
|
385
446
|
? path.join(env.CLAUDE_CONFIG_DIR, ".credentials.json")
|
|
386
447
|
: path.join(home, descriptor.credentialPath);
|
|
387
|
-
|
|
448
|
+
// Claude Code is asked with the participant's own environment, so a login that only works
|
|
449
|
+
// through a dropped variable (ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN) reads as signed out.
|
|
450
|
+
const probeEnv = descriptor.id === "claude" ? claudeParticipantEnv(env) : env;
|
|
451
|
+
const status = await probe(binPath, descriptor.id === "codex" ? ["login", "status"] : ["auth", "status"], probeEnv)
|
|
388
452
|
.then((result) => classifyAuth(descriptor.id, result))
|
|
389
453
|
.catch(() => ({ authStatus: "unknown" }));
|
|
454
|
+
const version = descriptor.id === "claude"
|
|
455
|
+
? await probe(binPath, ["--version"], probeEnv)
|
|
456
|
+
.then((result) => result.code === 0 ? parseClaudeCodeVersion(result.stdout) : undefined)
|
|
457
|
+
.catch(() => undefined)
|
|
458
|
+
: undefined;
|
|
390
459
|
found.push({
|
|
391
460
|
...descriptor,
|
|
392
461
|
binPath,
|
|
393
462
|
credentialsPresent: await exists(file).catch(() => false),
|
|
394
463
|
...status,
|
|
464
|
+
...(version === undefined ? {} : { version }),
|
|
395
465
|
});
|
|
396
466
|
}
|
|
397
467
|
return found;
|
|
398
468
|
}
|
|
399
469
|
/** Doctor's row when neither Codex nor Claude Code is installed. */
|
|
400
470
|
export const NO_LOCAL_AGENT_MESSAGE = "no local coding agent found. openai-computer-use needs OPENAI_API_KEY; local-agent needs Codex or Claude Code installed and signed in. Hosted desktops also need E2B_API_KEY.";
|
|
471
|
+
/** Whether a signed-in agent can still not run participants here: an old Claude Code. */
|
|
472
|
+
export function localAgentVersionRefusal(agent) {
|
|
473
|
+
return agent.id === "claude" && !admitsClaudeCodeVersion(agent.version)
|
|
474
|
+
? claudeCodeVersionMessage(agent.version)
|
|
475
|
+
: undefined;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Whether an installed agent can run participants here: it reports signed in under the
|
|
479
|
+
* participant's environment, and a Claude Code is at or above the floor. Doctor's rows decide their
|
|
480
|
+
* status from this, so a row whose message says the agent cannot run participants is never "ok".
|
|
481
|
+
*/
|
|
482
|
+
export function localAgentReady(agent) {
|
|
483
|
+
return agent.authStatus === "authenticated" && localAgentVersionRefusal(agent) === undefined;
|
|
484
|
+
}
|
|
401
485
|
/** Doctor's row for one installed agent, in the register the other rows use. */
|
|
402
486
|
export function localAgentDoctorMessage(agent) {
|
|
487
|
+
const outdated = localAgentVersionRefusal(agent);
|
|
488
|
+
if (agent.authStatus === "authenticated" && outdated !== undefined)
|
|
489
|
+
return `${agent.label} reports signed in, but cannot run participants yet. ${outdated}`;
|
|
403
490
|
if (agent.authStatus === "authenticated")
|
|
404
|
-
return `${agent.label} reports signed in; a study with
|
|
491
|
+
return `${agent.label}${agent.version === undefined ? "" : ` ${agent.version}`} reports signed in; a study with actor.type: local-agent can use it instead of a provider API key. Account access and limits are untested.`;
|
|
405
492
|
const status = agent.id === "codex" ? "codex login status" : "claude auth status";
|
|
406
493
|
return agent.authStatus === "unauthenticated"
|
|
407
494
|
? `${agent.label} is installed and reports not signed in; run \`${agent.id === "codex" ? "codex login" : "claude auth login"}\``
|
|
@@ -11,12 +11,12 @@ export type LocalAgentRefusal =
|
|
|
11
11
|
kind: "signin-required";
|
|
12
12
|
message: string;
|
|
13
13
|
}
|
|
14
|
-
/** Hosted Codex on an unsupported platform or a
|
|
14
|
+
/** Hosted Codex on an unsupported platform or release, or a Claude Code older than the floor. */
|
|
15
15
|
| {
|
|
16
16
|
kind: "unsupported";
|
|
17
17
|
message: string;
|
|
18
18
|
}
|
|
19
|
-
/** A ChatGPT-account Codex with
|
|
19
|
+
/** A ChatGPT-account Codex with a dollar cap in `caps`, which it cannot price. */
|
|
20
20
|
| {
|
|
21
21
|
kind: "unpriced-cap";
|
|
22
22
|
message: string;
|
|
@@ -1,44 +1,52 @@
|
|
|
1
1
|
// Whether a local-agent participant can run on this machine before anything is acquired: its CLI
|
|
2
|
-
// is on `PATH` and signed in, a hosted Codex CLI is a qualified release on a supported platform,
|
|
3
|
-
//
|
|
4
|
-
// the refusal's kind to its own
|
|
2
|
+
// is on `PATH` and signed in, a hosted Codex CLI is a qualified release on a supported platform, a
|
|
3
|
+
// Claude Code CLI has the flags that restrict a participant, and a ChatGPT-account Codex is not
|
|
4
|
+
// asked to enforce a dollar cap it has no price for. Each route maps the refusal's kind to its own
|
|
5
|
+
// error code.
|
|
5
6
|
import { describeCodexCliAdmission } from "../codex/codex-admission.js";
|
|
6
|
-
import { checkHostedCodexCompatibility, detectLocalAgents } from "./cli.js";
|
|
7
|
+
import { checkHostedCodexCompatibility, detectLocalAgents, localAgentVersionRefusal, } from "./cli.js";
|
|
8
|
+
import { cli } from "../../cli/invocation.js";
|
|
7
9
|
export async function localAgentRefusal(args) {
|
|
8
10
|
const { agent, env, caps } = args;
|
|
9
|
-
const available = await detectLocalAgents({ env });
|
|
11
|
+
const available = await detectLocalAgents({ env, only: agent });
|
|
10
12
|
const chosen = available.find((candidate) => candidate.id === agent);
|
|
11
13
|
if (chosen === undefined) {
|
|
12
14
|
return {
|
|
13
15
|
kind: "agent-missing",
|
|
14
|
-
message: `
|
|
15
|
-
`Install it, or set OPENAI_API_KEY and use
|
|
16
|
+
message: `actor.type: local-agent needs the ${agent} CLI on PATH and signed in. ` +
|
|
17
|
+
`Install it, or set OPENAI_API_KEY and use actor.type: openai-computer-use instead.`,
|
|
16
18
|
};
|
|
17
19
|
}
|
|
18
20
|
if (chosen.authStatus !== "authenticated") {
|
|
19
21
|
return {
|
|
20
22
|
kind: "signin-required",
|
|
21
23
|
message: chosen.authStatus === "unauthenticated"
|
|
22
|
-
?
|
|
24
|
+
? chosen.id === "codex"
|
|
25
|
+
? "Codex reports it is not signed in; run `codex login`, then retry."
|
|
26
|
+
: `Claude Code reports it is not signed in with the environment a participant gets, which keeps its own login but not ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN. Run \`claude auth login\`, then retry.`
|
|
23
27
|
: `${chosen.label} authentication status could not be checked. Run \`${chosen.id === "codex" ? "codex login status" : "claude auth status"}\` and update the CLI if needed. No desktop was launched.`,
|
|
24
28
|
};
|
|
25
29
|
}
|
|
26
|
-
if (chosen.id
|
|
27
|
-
|
|
30
|
+
if (chosen.id === "claude") {
|
|
31
|
+
const outdated = localAgentVersionRefusal(chosen);
|
|
32
|
+
return outdated === undefined
|
|
33
|
+
? undefined
|
|
34
|
+
: { kind: "unsupported", message: `${outdated} No desktop was launched.` };
|
|
35
|
+
}
|
|
28
36
|
const compatibility = await checkHostedCodexCompatibility(chosen.binPath, { env });
|
|
29
37
|
if (compatibility !== "supported") {
|
|
30
38
|
return {
|
|
31
39
|
kind: "unsupported",
|
|
32
40
|
message: compatibility === "unsupported_platform"
|
|
33
41
|
? `Hosted Codex participants require Linux or macOS on x64 or arm64. This host is ${process.platform}/${process.arch}; no desktop was launched.`
|
|
34
|
-
: `Hosted Codex participants need a Codex CLI release humanish runs: ${describeCodexCliAdmission()}. Run
|
|
42
|
+
: `Hosted Codex participants need a Codex CLI release humanish runs: ${describeCodexCliAdmission()}. Run \`${cli("doctor")}\` for the release it found and the command that replaces it; no desktop was launched.`,
|
|
35
43
|
};
|
|
36
44
|
}
|
|
37
45
|
if (chosen.billing === "account-unknown" &&
|
|
38
46
|
(caps.maxUsd !== undefined || caps.maxTotalUsd !== undefined)) {
|
|
39
47
|
return {
|
|
40
48
|
kind: "unpriced-cap",
|
|
41
|
-
message: "A ChatGPT-account Codex participant has no API-dollar price, so
|
|
49
|
+
message: "A ChatGPT-account Codex participant has no API-dollar price, so caps.maxUsd/maxTotalUsd cannot be enforced. Remove the dollar cap and use finite execution timeout/step limits, or use an API-backed participant; no desktop was launched.",
|
|
42
50
|
};
|
|
43
51
|
}
|
|
44
52
|
return undefined;
|
|
@@ -5,7 +5,8 @@ import { analyzeRun, correctAnalysis, dryRunBundleRefusal, showAnalysis, } from
|
|
|
5
5
|
import { listAnalyses } from "../../analysis/store.js";
|
|
6
6
|
import { listAnalysisExecutions } from "../../analysis/store-executions.js";
|
|
7
7
|
import { resolveRunPath } from "../../run/locate.js";
|
|
8
|
-
import { CWD_OPTION_DESCRIPTION, discoverCliKeys, JSON_OPTION_DESCRIPTION, RUN_OPTION_DESCRIPTION, writeResult, humanError, } from "../io.js";
|
|
8
|
+
import { CWD_OPTION_DESCRIPTION, discoverCliKeys, JSON_OPTION_DESCRIPTION, RUN_OPTION_DESCRIPTION, writeResult, humanError, wantsJson, } from "../io.js";
|
|
9
|
+
import { analysisFindings, formatFindings, missingRunFindings, readRunAnalysis, } from "../findings.js";
|
|
9
10
|
import { resolvePhysicalCwd } from "../../run/paths.js";
|
|
10
11
|
/** Commander may collect a shared flag on the parent; only explicit values override leaf defaults. */
|
|
11
12
|
function analysisSelection(options, command) {
|
|
@@ -46,7 +47,7 @@ export function registerAnalyzeCommand(parent, io) {
|
|
|
46
47
|
.action((options, command) => handleAnalyzeList(io, options, command));
|
|
47
48
|
analyze
|
|
48
49
|
.command("show")
|
|
49
|
-
.description("
|
|
50
|
+
.description("Print a run's analysis findings with the evidence each one cites. --json prints the validated analysis record and its correction history. Defaults to the latest usable version.")
|
|
50
51
|
.option("--run <id>", RUN_OPTION_DESCRIPTION, "latest")
|
|
51
52
|
.option("--id <id>", "Exact analysis version.")
|
|
52
53
|
.option("--cwd <path>", CWD_OPTION_DESCRIPTION, ".")
|
|
@@ -180,9 +181,24 @@ async function handleAnalyzeList(io, options, command) {
|
|
|
180
181
|
}
|
|
181
182
|
async function handleAnalyzeShow(io, options, command) {
|
|
182
183
|
options = analysisSelection(options, command);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
if (wantsJson(command)) {
|
|
185
|
+
const result = await showAnalysis(options.cwd, options.run, options.id);
|
|
186
|
+
writeResult(command, io, result, () => "");
|
|
187
|
+
io.setExitCode(result.state === "invalid" ? 2 : 0);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const source = await readRunAnalysis(options.cwd, options.run, options.id);
|
|
191
|
+
const findings = source ? analysisFindings(source) : missingRunFindings(options.cwd, options.run);
|
|
192
|
+
writeResult(command, io, findings, formatAnalysisShowHuman);
|
|
193
|
+
// The exit code the --json form gives for the same record.
|
|
194
|
+
io.setExitCode(source === null || source.loaded.state === "invalid" ? 2 : 0);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* `analyze show` without --json: the findings, or why there are none and the command that gets
|
|
198
|
+
* them.
|
|
199
|
+
*/
|
|
200
|
+
function formatAnalysisShowHuman(findings) {
|
|
201
|
+
return ([`humanish analyze show ${findings.runId}`, "", ...formatFindings(findings)].join("\n") + "\n");
|
|
186
202
|
}
|
|
187
203
|
async function handleAnalyzeCorrect(io, options, command) {
|
|
188
204
|
options = analysisSelection(options, command);
|
|
@@ -4,7 +4,7 @@ import { SERVE_SCHEMA, serveObserverLibrary } from "../../observer/serve.js";
|
|
|
4
4
|
import { startExposedObserver, validateExposure } from "../../observer/exposure.js";
|
|
5
5
|
import { ServeTunnelError } from "../../observer/tunnel.js";
|
|
6
6
|
import { collectRepeated, CWD_OPTION_DESCRIPTION, formatRunHuman, freePortOption, JSON_OPTION_DESCRIPTION, parseObserverPort, RUN_OPTION_DESCRIPTION, wantsJson, writeResult, } from "../io.js";
|
|
7
|
-
import { exitCodeForSignal, formatObserverHuman, } from "../observer-follow.js";
|
|
7
|
+
import { exitCodeForSignal, formatObserverHuman, personAtTerminal, unattendedObserverWarning, } from "../observer-follow.js";
|
|
8
8
|
export function registerObserveCommand(parent, io) {
|
|
9
9
|
addLibraryOptions(parent
|
|
10
10
|
.command("observe")
|
|
@@ -16,6 +16,7 @@ export function registerObserveCommand(parent, io) {
|
|
|
16
16
|
.option("--cwd <path>", CWD_OPTION_DESCRIPTION, ".")
|
|
17
17
|
.option("--open", "Open the Observer in the default browser.")
|
|
18
18
|
.option("--no-open", "Serve without opening a browser.")
|
|
19
|
+
.option("--serve", "Serve one run until Ctrl-C even without an interactive terminal; by default an agent's shell or a pipe gets the Observer path and an exit.")
|
|
19
20
|
.option("--json", JSON_OPTION_DESCRIPTION))
|
|
20
21
|
.addHelpText("after", [
|
|
21
22
|
"",
|
|
@@ -32,6 +33,8 @@ export function registerObserveCommand(parent, io) {
|
|
|
32
33
|
"The server binds 127.0.0.1 only. One run exposes just that run's bundle directory; --all",
|
|
33
34
|
"serves the run library. It stays attached until Ctrl-C; file:// security policy and live",
|
|
34
35
|
"refresh are why loopback http is preferred over opening the index.html path directly.",
|
|
36
|
+
"Without an interactive terminal, as in an agent's shell or a pipe, observe for one run",
|
|
37
|
+
"prints the Observer path and exits; --serve keeps it serving.",
|
|
35
38
|
"",
|
|
36
39
|
"--safe, --expose, --tunnel, --tunnel-domain, --oauth, --allow-email, --allow-domain and",
|
|
37
40
|
"--public-url need --all. Exposure only ever happens through an authenticated edge (ngrok",
|
|
@@ -119,9 +122,19 @@ async function observeRun(io, options, command) {
|
|
|
119
122
|
io.setExitCode(2);
|
|
120
123
|
return;
|
|
121
124
|
}
|
|
125
|
+
const wantsMachine = wantsJson(command);
|
|
126
|
+
if (options.serve !== true && !personAtTerminal()) {
|
|
127
|
+
// Nobody can press Ctrl-C here, so a server would wait forever: print the path and exit.
|
|
128
|
+
const result = {
|
|
129
|
+
...rendered,
|
|
130
|
+
warnings: [...rendered.warnings, unattendedObserverWarning("observe")],
|
|
131
|
+
};
|
|
132
|
+
writeResult(command, io, result, formatObserverHuman);
|
|
133
|
+
io.setExitCode(0);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
122
136
|
// Reuse the contained current-data projection, scoped to this run. A raw static
|
|
123
137
|
// server would miss runtime status and could replay stored iframe grants.
|
|
124
|
-
const wantsMachine = wantsJson(command);
|
|
125
138
|
const shouldOpen = options.open === false
|
|
126
139
|
? false
|
|
127
140
|
: options.open === true
|
|
@@ -11,6 +11,8 @@ export interface RunSignalOptions {
|
|
|
11
11
|
exit?: (code: number) => void;
|
|
12
12
|
reclaim?: ReclaimHooks;
|
|
13
13
|
deadlineMs?: number;
|
|
14
|
+
/** Resolves when the deadline has passed; defaults to a timer of `deadlineMs`. */
|
|
15
|
+
deadline?: (ms: number) => Promise<void>;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* Register work a committed shutdown finishes before it exits, such as closing watch's Observer
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
// The run command's signal handling outside post-run analysis. The first interrupt, terminate or
|
|
2
|
-
// hangup signal
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// hangup signal prints one line saying humanish is stopping, refuses any further sandbox create,
|
|
3
|
+
// marks each unfinished run in this process interrupted, reclaims its sandboxes (by receipt, by
|
|
4
|
+
// the ids its in-flight creates report, and by its E2B tags), runs the registered shutdown
|
|
5
|
+
// cleanups (watch's Observer server and tunnel), sweeps the interrupted runs' files of their raw
|
|
6
|
+
// sandbox ids as Run.finish does for a finished run, and exits 128+n, all within one deadline. A
|
|
7
|
+
// second signal prints the reclaim command for each run not yet reclaimed and exits at once.
|
|
8
|
+
// Routes take no abort signal, so the bundle is not finished, but the record ends at once and no
|
|
9
|
+
// sandbox the run created waits for its timeout. Analysis has its own cancel handlers
|
|
7
10
|
// (analysis-signals.ts) and takes over when it starts.
|
|
8
11
|
import path from "node:path";
|
|
9
12
|
import { activeRuns } from "../../run/active-runs.js";
|
|
10
13
|
import { LATEST_RUN_ALIAS } from "../../run/locate.js";
|
|
11
14
|
import { reclaimPinnedRunSandboxes, } from "../../run/reclaim.js";
|
|
15
|
+
import { stopSandboxCreates } from "../../run/sandbox-creates.js";
|
|
16
|
+
import { scrubRunSandboxIds } from "../../run/sandbox-ids.js";
|
|
12
17
|
import { SANDBOX_RECEIPTS_ARTIFACT } from "../../run/sandbox-receipts.js";
|
|
18
|
+
import { plural } from "../../run/text.js";
|
|
19
|
+
import { shellArg } from "../../substrates/shell.js";
|
|
13
20
|
import { exitCodeForSignal } from "../observer-follow.js";
|
|
21
|
+
import { cli } from "../invocation.js";
|
|
14
22
|
/** How long the handler waits for interrupt, reclaim and cleanups before it exits anyway. */
|
|
15
23
|
const INTERRUPT_RECLAIM_DEADLINE_MS = 10_000;
|
|
16
|
-
/** Each kill's own request timeout, inside the deadline. */
|
|
24
|
+
/** Each kill's own request timeout, inside the deadline. Kills run concurrently. */
|
|
17
25
|
const KILL_REQUEST_TIMEOUT_MS = 8_000;
|
|
26
|
+
/** How long reclaim waits for in-flight creates before it searches E2B by tag anyway. */
|
|
27
|
+
const CREATES_WAIT_MS = 6_000;
|
|
18
28
|
const SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
19
29
|
let current;
|
|
20
30
|
const shutdownCleanups = new Set();
|
|
@@ -39,7 +49,7 @@ export function beginRunSignalPhase(io, options = {}) {
|
|
|
39
49
|
process.exit(code);
|
|
40
50
|
});
|
|
41
51
|
const handlers = new Map();
|
|
42
|
-
let stopping
|
|
52
|
+
let stopping;
|
|
43
53
|
const end = () => {
|
|
44
54
|
for (const [signal, handler] of handlers)
|
|
45
55
|
target.removeListener(signal, handler);
|
|
@@ -48,16 +58,23 @@ export function beginRunSignalPhase(io, options = {}) {
|
|
|
48
58
|
current = undefined;
|
|
49
59
|
};
|
|
50
60
|
const release = () => {
|
|
51
|
-
if (
|
|
61
|
+
if (stopping === undefined)
|
|
52
62
|
end();
|
|
53
63
|
};
|
|
54
64
|
const onSignal = (signal) => {
|
|
55
|
-
if (stopping) {
|
|
65
|
+
if (stopping !== undefined) {
|
|
66
|
+
const open = stopping.filter((stop) => stop.report !== null && stop.report?.clean !== true);
|
|
67
|
+
for (const stop of open)
|
|
68
|
+
say(io, `humanish: second ${signal}: exiting before run ${stop.run.runId}'s sandboxes were confirmed stopped; ${recovery(stop.run)} to stop what is left.`);
|
|
69
|
+
if (open.length === 0)
|
|
70
|
+
say(io, `humanish: second ${signal}: exiting.`);
|
|
56
71
|
exit(exitCodeForSignal(signal));
|
|
57
72
|
return;
|
|
58
73
|
}
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
// Synchronous, before anything is awaited: no create can start after this point.
|
|
75
|
+
stopping = activeRuns().map((run) => ({ run, creates: stopSandboxCreates(run.paths) }));
|
|
76
|
+
say(io, stoppingLine(signal, stopping, options.deadlineMs ?? INTERRUPT_RECLAIM_DEADLINE_MS));
|
|
77
|
+
void stopActiveRuns(io, signal, stopping, options)
|
|
61
78
|
.catch(() => undefined)
|
|
62
79
|
.finally(() => exit(exitCodeForSignal(signal)));
|
|
63
80
|
};
|
|
@@ -74,48 +91,71 @@ export function beginRunSignalPhase(io, options = {}) {
|
|
|
74
91
|
export function handOverRunSignals() {
|
|
75
92
|
current?.release();
|
|
76
93
|
}
|
|
94
|
+
function say(io, line) {
|
|
95
|
+
try {
|
|
96
|
+
io.writeErr(`${line}\n`);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// A hung-up terminal cannot take the line; the status record and reclaim receipt hold it.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function stoppingLine(signal, stopping, deadlineMs) {
|
|
103
|
+
const again = `a second ${signal} exits without waiting`;
|
|
104
|
+
if (stopping.length === 0)
|
|
105
|
+
return `humanish: ${signal}: stopping; no run had started.`;
|
|
106
|
+
const runs = stopping.map((stop) => stop.run.runId).join(", ");
|
|
107
|
+
return `humanish: ${signal}: stopping run ${runs} and reclaiming its sandboxes (up to ${deadlineMs / 1000} s); ${again}.`;
|
|
108
|
+
}
|
|
77
109
|
/**
|
|
78
110
|
* Interrupt, reclaim and the registered cleanups all run inside one deadline that starts before
|
|
79
111
|
* the first status write is awaited, so a stalled write or kill still ends in a bounded exit.
|
|
80
112
|
*/
|
|
81
|
-
async function stopActiveRuns(io, signal, options) {
|
|
113
|
+
async function stopActiveRuns(io, signal, stopping, options) {
|
|
82
114
|
const deadlineMs = options.deadlineMs ?? INTERRUPT_RECLAIM_DEADLINE_MS;
|
|
83
|
-
const runs = activeRuns();
|
|
84
|
-
// Absent: still pending at the deadline. Null: the run had finished, so there is nothing to say.
|
|
85
|
-
const reports = new Map();
|
|
86
115
|
const work = Promise.all([
|
|
87
|
-
...
|
|
116
|
+
...stopping.map(async (stop) => {
|
|
88
117
|
// False when the run had already finished: its route released what it held.
|
|
89
|
-
if (!(await run.status.interrupt(signal))) {
|
|
90
|
-
|
|
118
|
+
if (!(await stop.run.status.interrupt(signal))) {
|
|
119
|
+
stop.report = null;
|
|
91
120
|
return;
|
|
92
121
|
}
|
|
93
|
-
const reclaimed = await reclaimPinnedRunSandboxes(run.cwd, run.paths, {
|
|
122
|
+
const reclaimed = await reclaimPinnedRunSandboxes(stop.run.cwd, stop.run.paths, {
|
|
94
123
|
requestTimeoutMs: KILL_REQUEST_TIMEOUT_MS,
|
|
124
|
+
createsWaitMs: Math.min(CREATES_WAIT_MS, deadlineMs / 2),
|
|
95
125
|
...options.reclaim,
|
|
126
|
+
creates: stop.creates,
|
|
96
127
|
}).catch(() => undefined);
|
|
97
|
-
|
|
128
|
+
stop.report = reclaimSummary(stop.run, reclaimed);
|
|
98
129
|
}),
|
|
99
130
|
...[...shutdownCleanups].map((cleanup) => cleanup().catch(() => undefined)),
|
|
100
|
-
])
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
131
|
+
]).then(() =>
|
|
132
|
+
// Last, so it also covers what the route recorded while reclaim ran, such as a desktop
|
|
133
|
+
// startup error that quotes the sandbox the kill cut short. A run stopped by the deadline is
|
|
134
|
+
// not swept; verify grades a file that names a receipt's id local_only.
|
|
135
|
+
Promise.all(stopping
|
|
136
|
+
.filter((stop) => stop.report !== null)
|
|
137
|
+
.map((stop) => scrubRunSandboxIds(stop.run.paths).catch(() => undefined))));
|
|
138
|
+
const finished = await withDeadline(work, deadlineMs, options.deadline);
|
|
139
|
+
for (const stop of stopping) {
|
|
140
|
+
if (stop.report === null)
|
|
105
141
|
continue;
|
|
106
|
-
const text = report ??
|
|
107
|
-
|
|
108
|
-
? `stopping did not finish within ${deadlineMs / 1000} s; ${recovery(run)}.`
|
|
109
|
-
: `reclaim failed; ${recovery(run)}.`);
|
|
110
|
-
try {
|
|
111
|
-
io.writeErr(`humanish: ${signal}: run ${run.runId} marked interrupted; ${text}\n`);
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
// A hung-up terminal cannot take the line; the status record and reclaim receipt hold it.
|
|
115
|
-
}
|
|
142
|
+
const text = stop.report?.text ?? deadlineText(stop, finished, deadlineMs);
|
|
143
|
+
say(io, `humanish: ${signal}: run ${stop.run.runId} marked interrupted; ${text}`);
|
|
116
144
|
}
|
|
117
145
|
}
|
|
118
|
-
function
|
|
146
|
+
function deadlineText(stop, finished, deadlineMs) {
|
|
147
|
+
if (finished !== "deadline")
|
|
148
|
+
return `reclaim failed; sandboxes unknown; ${recovery(stop.run)}.`;
|
|
149
|
+
const inFlight = stop.creates.inFlight();
|
|
150
|
+
const creates = inFlight === 0 ? "" : ` Still waiting on E2B: ${plural(inFlight, "sandbox create")}.`;
|
|
151
|
+
return `stopping did not finish within ${deadlineMs / 1000} s; sandboxes unknown.${creates} ${capitalize(recovery(stop.run))}; it also finds sandboxes by this run's E2B tags.`;
|
|
152
|
+
}
|
|
153
|
+
function capitalize(text) {
|
|
154
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
155
|
+
}
|
|
156
|
+
function withDeadline(work, ms, wait) {
|
|
157
|
+
if (wait !== undefined)
|
|
158
|
+
return Promise.race([work, wait(ms).then(() => "deadline")]);
|
|
119
159
|
let timer;
|
|
120
160
|
const deadline = new Promise((resolve) => {
|
|
121
161
|
timer = setTimeout(() => resolve("deadline"), ms);
|
|
@@ -127,20 +167,41 @@ function withDeadline(work, ms) {
|
|
|
127
167
|
* to reclaim, which would resolve the newest run instead, so it gets its receipts path.
|
|
128
168
|
*/
|
|
129
169
|
function recovery(run) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
170
|
+
if (run.runId === LATEST_RUN_ALIAS)
|
|
171
|
+
return `its id is \`${LATEST_RUN_ALIAS}\`, which \`${cli("reclaim --run")}\` reads as the newest run, so kill the ids in ${path.join(run.paths.relativeRunRoot, SANDBOX_RECEIPTS_ARTIFACT)} by hand`;
|
|
172
|
+
const cwd = path.resolve(run.cwd) === process.cwd() ? "" : ` --cwd ${shellArg(run.cwd)}`;
|
|
173
|
+
return `run \`${cli(`reclaim --run ${shellArg(run.runId)}${cwd}`)}\``;
|
|
133
174
|
}
|
|
134
175
|
function reclaimSummary(run, reclaimed) {
|
|
135
176
|
if (reclaimed === undefined)
|
|
136
|
-
return `reclaim failed; ${recovery(run)}
|
|
177
|
+
return { text: `reclaim failed; sandboxes unknown; ${recovery(run)}.`, clean: false };
|
|
137
178
|
if (reclaimed.error !== undefined)
|
|
138
|
-
return
|
|
139
|
-
|
|
140
|
-
|
|
179
|
+
return {
|
|
180
|
+
text: `${reclaimed.error.message} Sandboxes unknown; ${recovery(run)}.`,
|
|
181
|
+
clean: false,
|
|
182
|
+
};
|
|
141
183
|
const counts = new Map();
|
|
142
184
|
for (const outcome of reclaimed.outcomes)
|
|
143
185
|
counts.set(outcome.state, (counts.get(outcome.state) ?? 0) + 1);
|
|
144
|
-
const tally =
|
|
145
|
-
|
|
186
|
+
const tally = counts.size === 0
|
|
187
|
+
? "none created"
|
|
188
|
+
: [...counts].map(([state, count]) => `${count} ${state}`).join(", ");
|
|
189
|
+
switch (reclaimed.state) {
|
|
190
|
+
case "clean":
|
|
191
|
+
return {
|
|
192
|
+
text: `sandboxes clean: ${tally}; E2B lists none still tagged with this run.`,
|
|
193
|
+
clean: true,
|
|
194
|
+
};
|
|
195
|
+
case "unconfirmed":
|
|
196
|
+
return { text: `sandboxes unconfirmed: ${tally}; ${recovery(run)} to retry.`, clean: false };
|
|
197
|
+
default: {
|
|
198
|
+
const why = reclaimed.createsInFlight > 0
|
|
199
|
+
? `${plural(reclaimed.createsInFlight, "sandbox create")} still waiting on E2B`
|
|
200
|
+
: `the E2B tag search ${reclaimed.tagSearch.status}${reclaimed.tagSearch.detail === undefined ? "" : ` (${reclaimed.tagSearch.detail})`}`;
|
|
201
|
+
return {
|
|
202
|
+
text: `sandboxes unknown: ${tally}, but ${why}; ${recovery(run)}.`,
|
|
203
|
+
clean: false,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
146
207
|
}
|
|
@@ -2,6 +2,11 @@ import { Command } from "commander";
|
|
|
2
2
|
import { type CliIo } from "../io.js";
|
|
3
3
|
export declare function registerRunCommand(parent: Command, io: CliIo): void;
|
|
4
4
|
export declare function registerVerifyCommand(parent: Command, io: CliIo): void;
|
|
5
|
+
/**
|
|
6
|
+
* The deprecated `cleanup`, hidden from help: one warning, then exactly `reclaim --check`, with
|
|
7
|
+
* its output and exit code. The old command read only what computer-use runs recorded and passed
|
|
8
|
+
* when it found no record; the check asks E2B.
|
|
9
|
+
*/
|
|
5
10
|
export declare function registerCleanupCommand(parent: Command, io: CliIo): void;
|
|
6
11
|
export declare function registerReviewCommand(parent: Command, io: CliIo): void;
|
|
7
12
|
export declare function registerExportCommand(parent: Command, io: CliIo): void;
|