ccqa 1.40.0 → 1.40.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/dist/bin/ccqa.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { HubApiError, createHubClient, hubRequest } from "../hub-client/index.mjs";
|
|
3
3
|
import { t as EVIDENCE_DIR_ENV } from "../evidence-constants-C425F7ZG.mjs";
|
|
4
|
-
import { a as formatAgentBrowserUnavailableMessage, i as assertAgentBrowserAvailable, n as spawnAB, o as pathWithAgentBrowserShim, r as AgentBrowserUnavailableError, s as resolveAgentBrowserBin$1, t as sleepSync } from "../spawn-ab-
|
|
4
|
+
import { a as formatAgentBrowserUnavailableMessage, i as assertAgentBrowserAvailable, n as spawnAB, o as pathWithAgentBrowserShim, r as AgentBrowserUnavailableError, s as resolveAgentBrowserBin$1, t as sleepSync } from "../spawn-ab-Bm34WBui.mjs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { accessSync, appendFileSync, createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -19,6 +19,7 @@ import { promisify } from "node:util";
|
|
|
19
19
|
import { createInterface } from "node:readline/promises";
|
|
20
20
|
import { createServer } from "node:http";
|
|
21
21
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
22
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
22
23
|
import { createInterface as createInterface$1 } from "node:readline";
|
|
23
24
|
import { createServer as createServer$1 } from "node:net";
|
|
24
25
|
//#region src/run/report-constants.ts
|
|
@@ -1615,10 +1616,17 @@ function warnOnceIfNativeBinaryMissing() {
|
|
|
1615
1616
|
const missing = missingNativeBinaryPackage();
|
|
1616
1617
|
if (missing) warn(missingNativeBinaryMessage(missing));
|
|
1617
1618
|
}
|
|
1619
|
+
/** Whole minutes read best, but the ceiling is set in ms and may be seconds. */
|
|
1620
|
+
function formatDuration$1(ms) {
|
|
1621
|
+
if (ms < 6e4 || ms % 6e4 !== 0) return `${Math.round(ms / 1e3)}s`;
|
|
1622
|
+
const minutes = ms / 6e4;
|
|
1623
|
+
return `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
1624
|
+
}
|
|
1618
1625
|
async function invokeClaudeStreaming(options, onEvent) {
|
|
1619
|
-
const { prompt, systemPrompt, allowedTools, disableBuiltinTools = false, disableThinking = false, mcpServers, maxTurns, env, model, cwd, onAbAction, onAbActionFailed, silenceBashLog = false, envScrubMap = [], relaxAbConstraints = false } = options;
|
|
1626
|
+
const { prompt, systemPrompt, allowedTools, disableBuiltinTools = false, disableThinking = false, mcpServers, maxTurns, timeoutMs, env, model, cwd, onAbAction, onAbActionFailed, silenceBashLog = false, envScrubMap = [], relaxAbConstraints = false } = options;
|
|
1620
1627
|
const resolvedModel = resolveModel(model);
|
|
1621
1628
|
const mergedEnv = buildInvocationEnv(env);
|
|
1629
|
+
const abortController = new AbortController();
|
|
1622
1630
|
let lastAbToolUseId = null;
|
|
1623
1631
|
const claimAbToolUse = (toolUseId) => {
|
|
1624
1632
|
if (toolUseId !== lastAbToolUseId) return false;
|
|
@@ -1631,6 +1639,7 @@ async function invokeClaudeStreaming(options, onEvent) {
|
|
|
1631
1639
|
allowedTools: allowedTools ?? ["Bash(*)"],
|
|
1632
1640
|
permissionMode: "bypassPermissions",
|
|
1633
1641
|
allowDangerouslySkipPermissions: true,
|
|
1642
|
+
abortController,
|
|
1634
1643
|
...resolvedModel ? { model: resolvedModel } : {},
|
|
1635
1644
|
...cwd ? { cwd } : {},
|
|
1636
1645
|
...mergedEnv ? { env: mergedEnv } : {},
|
|
@@ -1695,7 +1704,10 @@ async function invokeClaudeStreaming(options, onEvent) {
|
|
|
1695
1704
|
} : void 0
|
|
1696
1705
|
};
|
|
1697
1706
|
warnOnceIfNativeBinaryMissing();
|
|
1707
|
+
const capTimer = timeoutMs === void 0 ? null : setTimeout(() => abortController.abort(), timeoutMs);
|
|
1708
|
+
capTimer?.unref?.();
|
|
1698
1709
|
let result = "";
|
|
1710
|
+
let answered = false;
|
|
1699
1711
|
let isError = false;
|
|
1700
1712
|
let errorDetail = null;
|
|
1701
1713
|
let cost = {
|
|
@@ -1720,6 +1732,7 @@ async function invokeClaudeStreaming(options, onEvent) {
|
|
|
1720
1732
|
}
|
|
1721
1733
|
}
|
|
1722
1734
|
if (msg.type === "result") {
|
|
1735
|
+
answered = true;
|
|
1723
1736
|
isError = msg.is_error ?? false;
|
|
1724
1737
|
if (msg.subtype === "success") result = msg.result;
|
|
1725
1738
|
else {
|
|
@@ -1733,6 +1746,13 @@ async function invokeClaudeStreaming(options, onEvent) {
|
|
|
1733
1746
|
isError = true;
|
|
1734
1747
|
errorDetail = err instanceof Error ? err.message : String(err);
|
|
1735
1748
|
if (!result) result = errorDetail;
|
|
1749
|
+
} finally {
|
|
1750
|
+
if (capTimer) clearTimeout(capTimer);
|
|
1751
|
+
}
|
|
1752
|
+
if (abortController.signal.aborted && timeoutMs !== void 0 && !answered) {
|
|
1753
|
+
isError = true;
|
|
1754
|
+
errorDetail = `stopped after ${formatDuration$1(timeoutMs)} (host time limit)`;
|
|
1755
|
+
result = errorDetail;
|
|
1736
1756
|
}
|
|
1737
1757
|
tallyInvocation(cost);
|
|
1738
1758
|
return {
|
|
@@ -10490,16 +10510,8 @@ async function removeTempStateDir(statePath) {
|
|
|
10490
10510
|
* than throwing; the caller decides whether an un-restored session is fatal.
|
|
10491
10511
|
*/
|
|
10492
10512
|
function loadStateIntoSession(sessionName, statePath) {
|
|
10493
|
-
const boot =
|
|
10494
|
-
|
|
10495
|
-
sessionName,
|
|
10496
|
-
"open",
|
|
10497
|
-
"about:blank"
|
|
10498
|
-
]);
|
|
10499
|
-
if (boot.status !== 0) return {
|
|
10500
|
-
ok: false,
|
|
10501
|
-
error: (boot.stderr || boot.stdout || `open exited ${boot.status}`).trim()
|
|
10502
|
-
};
|
|
10513
|
+
const boot = bootSession(sessionName);
|
|
10514
|
+
if (!boot.ok) return boot;
|
|
10503
10515
|
const load = spawnAB([
|
|
10504
10516
|
"--session",
|
|
10505
10517
|
sessionName,
|
|
@@ -10509,7 +10521,26 @@ function loadStateIntoSession(sessionName, statePath) {
|
|
|
10509
10521
|
]);
|
|
10510
10522
|
if (load.status !== 0) return {
|
|
10511
10523
|
ok: false,
|
|
10512
|
-
error: (load.stderr || load.stdout || `state load exited ${load.status}`).trim()
|
|
10524
|
+
error: (load.stderr || load.stdout || `state load exited ${load.status}`).trim(),
|
|
10525
|
+
wedged: load.wedged
|
|
10526
|
+
};
|
|
10527
|
+
return { ok: true };
|
|
10528
|
+
}
|
|
10529
|
+
/**
|
|
10530
|
+
* Bring up the session's daemon and browser without navigating, so whatever
|
|
10531
|
+
* the caller attaches next lands on the session rather than racing a page load.
|
|
10532
|
+
*/
|
|
10533
|
+
function bootSession(sessionName) {
|
|
10534
|
+
const boot = spawnAB([
|
|
10535
|
+
"--session",
|
|
10536
|
+
sessionName,
|
|
10537
|
+
"open",
|
|
10538
|
+
"about:blank"
|
|
10539
|
+
]);
|
|
10540
|
+
if (boot.status !== 0) return {
|
|
10541
|
+
ok: false,
|
|
10542
|
+
error: (boot.stderr || boot.stdout || `open exited ${boot.status}`).trim(),
|
|
10543
|
+
wedged: boot.wedged
|
|
10513
10544
|
};
|
|
10514
10545
|
return { ok: true };
|
|
10515
10546
|
}
|
|
@@ -10636,8 +10667,7 @@ function verifySessionRestores(statePath, verifyUrl) {
|
|
|
10636
10667
|
* false "unhealthy" would re-inject the saved state and wipe auth the spec
|
|
10637
10668
|
* acquired live — breaking a spec that was fine. Missing a same-origin-ish
|
|
10638
10669
|
* sign-in wall just leaves that step failing as before (no regression), so the
|
|
10639
|
-
* asymmetry favours only firing on a provably dead daemon.
|
|
10640
|
-
* required (it's the re-anchor target for recovery) but no longer compared here.
|
|
10670
|
+
* asymmetry favours only firing on a provably dead daemon.
|
|
10641
10671
|
*/
|
|
10642
10672
|
function checkLiveSessionHealth(sessionName) {
|
|
10643
10673
|
const probe = spawnAB([
|
|
@@ -10648,30 +10678,30 @@ function checkLiveSessionHealth(sessionName) {
|
|
|
10648
10678
|
]);
|
|
10649
10679
|
if (probe.status !== 0) return {
|
|
10650
10680
|
healthy: false,
|
|
10681
|
+
kind: probe.wedged === true ? "unresponsive" : "errored",
|
|
10651
10682
|
reason: (probe.stderr || probe.stdout || `probe exited ${probe.status}`).trim()
|
|
10652
10683
|
};
|
|
10653
10684
|
const href = unwrapEvalString(probe.stdout);
|
|
10654
10685
|
if (!href || href === "about:blank" || href.startsWith("chrome://") || href.startsWith("chrome-error://")) return {
|
|
10655
10686
|
healthy: false,
|
|
10687
|
+
kind: "blank",
|
|
10656
10688
|
reason: `blank/absent page (${href || "empty"})`
|
|
10657
10689
|
};
|
|
10658
10690
|
return { healthy: true };
|
|
10659
10691
|
}
|
|
10660
10692
|
/**
|
|
10661
|
-
*
|
|
10662
|
-
*
|
|
10663
|
-
*
|
|
10664
|
-
*
|
|
10665
|
-
*
|
|
10666
|
-
*
|
|
10667
|
-
*
|
|
10668
|
-
* injection result; the trailing `open` is best-effort (a failed nav still
|
|
10669
|
-
* leaves the state attached for the model's own next navigation).
|
|
10693
|
+
* Put a live session back on its feet after {@link checkLiveSessionHealth}
|
|
10694
|
+
* found it broken: boot its browser, re-attach the saved state if it has one,
|
|
10695
|
+
* and anchor on `verifyUrl` so a retrying model resumes from a signed-in page.
|
|
10696
|
+
*
|
|
10697
|
+
* An unresponsive daemon must already have been killed by the caller — every
|
|
10698
|
+
* command below goes through the socket it is ignoring. The trailing `open` is
|
|
10699
|
+
* best-effort; a failed nav still leaves a usable session behind.
|
|
10670
10700
|
*/
|
|
10671
10701
|
function recoverLiveSession(sessionName, statePath, verifyUrl) {
|
|
10672
|
-
const
|
|
10673
|
-
if (!
|
|
10674
|
-
spawnAB([
|
|
10702
|
+
const restored = statePath ? loadStateIntoSession(sessionName, statePath) : bootSession(sessionName);
|
|
10703
|
+
if (!restored.ok) return restored;
|
|
10704
|
+
if (verifyUrl) spawnAB([
|
|
10675
10705
|
"--session",
|
|
10676
10706
|
sessionName,
|
|
10677
10707
|
"open",
|
|
@@ -12051,6 +12081,146 @@ function buildLiveUserPrompt(step) {
|
|
|
12051
12081
|
return `Execute step ${step.id} and emit your STEP_RESULT verdict as instructed in the system prompt.`;
|
|
12052
12082
|
}
|
|
12053
12083
|
//#endregion
|
|
12084
|
+
//#region src/runtime/agent-browser-daemon.ts
|
|
12085
|
+
/**
|
|
12086
|
+
* Forcing a session's agent-browser daemon out when it has stopped answering.
|
|
12087
|
+
*
|
|
12088
|
+
* Every other way ccqa reaches a daemon is a command over that daemon's socket
|
|
12089
|
+
* — `close`, the only shutdown the CLI offers, included — so none of them work
|
|
12090
|
+
* on one that no longer reads it. The per-session pid file does not.
|
|
12091
|
+
*/
|
|
12092
|
+
/** Measured against agent-browser 0.26-0.34. */
|
|
12093
|
+
function agentBrowserRuntimeDir() {
|
|
12094
|
+
const explicit = process.env["AGENT_BROWSER_SOCKET_DIR"];
|
|
12095
|
+
const xdg = process.env["XDG_RUNTIME_DIR"];
|
|
12096
|
+
const base = explicit ?? (xdg ? join(xdg, "agent-browser") : join(homedir(), ".agent-browser"));
|
|
12097
|
+
const namespace = process.env["AGENT_BROWSER_NAMESPACE"];
|
|
12098
|
+
return namespace ? join(base, "namespaces", namespace, "run") : base;
|
|
12099
|
+
}
|
|
12100
|
+
/** The daemon pid agent-browser recorded for `sessionName`, if it wrote one. */
|
|
12101
|
+
function readDaemonPid(sessionName) {
|
|
12102
|
+
try {
|
|
12103
|
+
const pid = Number(readFileSync(join(agentBrowserRuntimeDir(), `${sessionName}.pid`), "utf8").trim());
|
|
12104
|
+
return Number.isInteger(pid) && pid > 1 ? pid : null;
|
|
12105
|
+
} catch {
|
|
12106
|
+
return null;
|
|
12107
|
+
}
|
|
12108
|
+
}
|
|
12109
|
+
function isAlive(pid) {
|
|
12110
|
+
try {
|
|
12111
|
+
process.kill(pid, 0);
|
|
12112
|
+
return true;
|
|
12113
|
+
} catch (e) {
|
|
12114
|
+
return e.code === "EPERM";
|
|
12115
|
+
}
|
|
12116
|
+
}
|
|
12117
|
+
/**
|
|
12118
|
+
* A pid file outlives an unclean exit, so its number may since have been handed
|
|
12119
|
+
* to something else. Where `ps` cannot answer — Windows has none — decline
|
|
12120
|
+
* rather than guess, which makes the kill a no-op instead of a hazard.
|
|
12121
|
+
*
|
|
12122
|
+
* Residual risk this cannot cover: a pid recycled onto *another session's*
|
|
12123
|
+
* daemon has identical argv and passes.
|
|
12124
|
+
*/
|
|
12125
|
+
function looksLikeAgentBrowser(pid) {
|
|
12126
|
+
const ps = spawnSync("ps", [
|
|
12127
|
+
"-p",
|
|
12128
|
+
String(pid),
|
|
12129
|
+
"-o",
|
|
12130
|
+
"args="
|
|
12131
|
+
], { encoding: "utf8" });
|
|
12132
|
+
return ps.status === 0 && ps.stdout.includes("agent-browser");
|
|
12133
|
+
}
|
|
12134
|
+
function childPids(parent) {
|
|
12135
|
+
const ps = spawnSync("ps", ["-eo", "pid=,ppid="], { encoding: "utf8" });
|
|
12136
|
+
if (ps.status !== 0) return [];
|
|
12137
|
+
const children = [];
|
|
12138
|
+
for (const line of ps.stdout.split("\n")) {
|
|
12139
|
+
const [pid, ppid] = line.trim().split(/\s+/).map(Number);
|
|
12140
|
+
if (pid && ppid === parent) children.push(pid);
|
|
12141
|
+
}
|
|
12142
|
+
return children;
|
|
12143
|
+
}
|
|
12144
|
+
/** Ramped so the common case — a daemon that exits at once — is not taxed. */
|
|
12145
|
+
const TERM_POLL_MS = [
|
|
12146
|
+
25,
|
|
12147
|
+
50,
|
|
12148
|
+
100,
|
|
12149
|
+
250,
|
|
12150
|
+
250,
|
|
12151
|
+
250,
|
|
12152
|
+
500,
|
|
12153
|
+
500,
|
|
12154
|
+
1e3,
|
|
12155
|
+
1e3,
|
|
12156
|
+
1e3
|
|
12157
|
+
];
|
|
12158
|
+
/** One phrase covering both outcomes, so callers log a single line. */
|
|
12159
|
+
function describeKill(kill) {
|
|
12160
|
+
return kill.killed ? `killed daemon pid ${kill.pid}` : `could not kill the daemon (${kill.reason})`;
|
|
12161
|
+
}
|
|
12162
|
+
/**
|
|
12163
|
+
* Stop the daemon serving `sessionName`. The session stays reusable —
|
|
12164
|
+
* agent-browser boots a fresh daemon under the same name and clears the stale
|
|
12165
|
+
* socket itself.
|
|
12166
|
+
*
|
|
12167
|
+
* SIGTERM is given time because a daemon that exits on its own signal takes its
|
|
12168
|
+
* browser down with it, while SIGKILL leaves that browser running with no owner.
|
|
12169
|
+
*/
|
|
12170
|
+
async function killSessionDaemon(sessionName) {
|
|
12171
|
+
const pid = readDaemonPid(sessionName);
|
|
12172
|
+
if (pid === null) return {
|
|
12173
|
+
killed: false,
|
|
12174
|
+
reason: "no pid file for this session"
|
|
12175
|
+
};
|
|
12176
|
+
if (!isAlive(pid)) return {
|
|
12177
|
+
killed: false,
|
|
12178
|
+
reason: `pid ${pid} is not running`
|
|
12179
|
+
};
|
|
12180
|
+
if (!looksLikeAgentBrowser(pid)) return {
|
|
12181
|
+
killed: false,
|
|
12182
|
+
reason: `pid ${pid} is not an agent-browser process`
|
|
12183
|
+
};
|
|
12184
|
+
try {
|
|
12185
|
+
process.kill(pid, "SIGTERM");
|
|
12186
|
+
} catch {
|
|
12187
|
+
return {
|
|
12188
|
+
killed: false,
|
|
12189
|
+
reason: `pid ${pid} could not be signalled`
|
|
12190
|
+
};
|
|
12191
|
+
}
|
|
12192
|
+
for (const wait of TERM_POLL_MS) {
|
|
12193
|
+
if (!isAlive(pid)) return {
|
|
12194
|
+
killed: true,
|
|
12195
|
+
pid
|
|
12196
|
+
};
|
|
12197
|
+
await setTimeout$1(wait);
|
|
12198
|
+
}
|
|
12199
|
+
if (!isAlive(pid)) return {
|
|
12200
|
+
killed: true,
|
|
12201
|
+
pid
|
|
12202
|
+
};
|
|
12203
|
+
const owned = childPids(pid);
|
|
12204
|
+
try {
|
|
12205
|
+
process.kill(pid, "SIGKILL");
|
|
12206
|
+
} catch {}
|
|
12207
|
+
for (const wait of TERM_POLL_MS) {
|
|
12208
|
+
if (!isAlive(pid)) break;
|
|
12209
|
+
await setTimeout$1(wait);
|
|
12210
|
+
}
|
|
12211
|
+
if (isAlive(pid)) return {
|
|
12212
|
+
killed: false,
|
|
12213
|
+
reason: `pid ${pid} survived SIGKILL`
|
|
12214
|
+
};
|
|
12215
|
+
for (const child of owned) if (isAlive(child)) try {
|
|
12216
|
+
process.kill(child, "SIGTERM");
|
|
12217
|
+
} catch {}
|
|
12218
|
+
return {
|
|
12219
|
+
killed: true,
|
|
12220
|
+
pid
|
|
12221
|
+
};
|
|
12222
|
+
}
|
|
12223
|
+
//#endregion
|
|
12054
12224
|
//#region src/runtime/live-result-parse.ts
|
|
12055
12225
|
const MAX_REASON_LEN = 2e3;
|
|
12056
12226
|
/** Parse a single STEP_RESULT line. Returns null on malformed input. */
|
|
@@ -12142,7 +12312,14 @@ async function runLiveExecutor(input) {
|
|
|
12142
12312
|
const retries = Math.max(0, input.retries ?? 0);
|
|
12143
12313
|
if (statePath) {
|
|
12144
12314
|
const injected = loadStateIntoSession(input.sessionName, statePath);
|
|
12145
|
-
if (!injected.ok
|
|
12315
|
+
if (!injected.ok && injected.wedged) {
|
|
12316
|
+
const kill = await killSessionDaemon(input.sessionName);
|
|
12317
|
+
warn(`session state restore failed: ${injected.error}; ${describeKill(kill)}`);
|
|
12318
|
+
if (kill.killed) {
|
|
12319
|
+
const retried = loadStateIntoSession(input.sessionName, statePath);
|
|
12320
|
+
if (!retried.ok) warn(`session state restore failed again: ${retried.error}`);
|
|
12321
|
+
}
|
|
12322
|
+
} else if (!injected.ok) warn(`session state restore failed: ${injected.error}`);
|
|
12146
12323
|
}
|
|
12147
12324
|
for (let i = 0; i < input.steps.length; i++) {
|
|
12148
12325
|
const step$1 = input.steps[i];
|
|
@@ -12163,15 +12340,18 @@ async function runLiveExecutor(input) {
|
|
|
12163
12340
|
for (;;) {
|
|
12164
12341
|
lastOutcome = await executeStepAttempt(step$1, paths, systemPrompt, userPrompt);
|
|
12165
12342
|
if (lastOutcome.status === "passed") break;
|
|
12166
|
-
if (!recoveredOnce
|
|
12343
|
+
if (!recoveredOnce) {
|
|
12344
|
+
recoveredOnce = true;
|
|
12167
12345
|
const health = checkLiveSessionHealth(input.sessionName);
|
|
12168
|
-
if (!health.healthy) {
|
|
12169
|
-
|
|
12170
|
-
|
|
12171
|
-
if (!
|
|
12172
|
-
|
|
12173
|
-
|
|
12174
|
-
|
|
12346
|
+
if (!health.healthy && (health.kind !== "blank" || statePath)) {
|
|
12347
|
+
const kill = health.kind === "unresponsive" ? await killSessionDaemon(input.sessionName) : null;
|
|
12348
|
+
warn(`session broken during ${step$1.id} (${health.reason}); ` + (kill ? describeKill(kill) : "re-injecting auth-state"));
|
|
12349
|
+
if (!kill || kill.killed) {
|
|
12350
|
+
const rec = recoverLiveSession(input.sessionName, statePath, verifyUrl);
|
|
12351
|
+
if (!rec.ok) warn(`session recovery failed: ${rec.error}`);
|
|
12352
|
+
attempt++;
|
|
12353
|
+
continue;
|
|
12354
|
+
}
|
|
12175
12355
|
}
|
|
12176
12356
|
}
|
|
12177
12357
|
if (attempt >= retries) break;
|
|
@@ -12215,7 +12395,8 @@ async function runLiveExecutor(input) {
|
|
|
12215
12395
|
systemPrompt,
|
|
12216
12396
|
model: input.model,
|
|
12217
12397
|
envScrubMap: input.envScrubMap,
|
|
12218
|
-
relaxAbConstraints: true
|
|
12398
|
+
relaxAbConstraints: true,
|
|
12399
|
+
timeoutMs: stepAttemptTimeoutMs()
|
|
12219
12400
|
}, (msg) => {
|
|
12220
12401
|
if (msg.type !== "assistant") return;
|
|
12221
12402
|
for (const block of msg.message.content ?? []) {
|
|
@@ -12271,6 +12452,18 @@ async function runLiveExecutor(input) {
|
|
|
12271
12452
|
* are the winning path — exactly the shortcut a later run should reuse.
|
|
12272
12453
|
*/
|
|
12273
12454
|
const MAX_LEARNED_COMMANDS = 15;
|
|
12455
|
+
/**
|
|
12456
|
+
* Wall-clock ceiling on one step attempt. The prompt's own ~3 minute wait
|
|
12457
|
+
* budget cannot end a step, because a model parked on a notification never
|
|
12458
|
+
* comes back to read it. Sized off measured runs: the longest passing step was
|
|
12459
|
+
* 4 minutes, against a wedged one that ran 15.
|
|
12460
|
+
*/
|
|
12461
|
+
const STEP_ATTEMPT_TIMEOUT_MS = 8 * 6e4;
|
|
12462
|
+
/** Env override so a slow environment can be tuned without a release. */
|
|
12463
|
+
function stepAttemptTimeoutMs() {
|
|
12464
|
+
const raw = Number(process.env["CCQA_LIVE_STEP_TIMEOUT_MS"]);
|
|
12465
|
+
return Number.isFinite(raw) && raw > 0 ? raw : STEP_ATTEMPT_TIMEOUT_MS;
|
|
12466
|
+
}
|
|
12274
12467
|
function emptyStepCost() {
|
|
12275
12468
|
return {
|
|
12276
12469
|
totalCostUsd: null,
|
|
@@ -12581,13 +12774,14 @@ function truncate(s, maxBytes) {
|
|
|
12581
12774
|
* Close an agent-browser session by name. Used before/after a `ccqa generate`
|
|
12582
12775
|
* run so a wedged daemon from a previous attempt can't hang the next one.
|
|
12583
12776
|
*
|
|
12584
|
-
* Always resolves; never throws. If the binary is missing
|
|
12585
|
-
* doesn't exist,
|
|
12586
|
-
*
|
|
12777
|
+
* Always resolves; never throws. If the binary is missing or the session
|
|
12778
|
+
* doesn't exist, we silently return — close is best-effort cleanup, not a
|
|
12779
|
+
* precondition.
|
|
12587
12780
|
*/
|
|
12588
12781
|
async function closeSession(sessionName) {
|
|
12589
12782
|
const abBin = resolveAgentBrowserBin();
|
|
12590
12783
|
if (!abBin) return;
|
|
12784
|
+
let unanswered = false;
|
|
12591
12785
|
await new Promise((resolve) => {
|
|
12592
12786
|
const child = spawn(process.execPath, [abBin, "close"], {
|
|
12593
12787
|
env: {
|
|
@@ -12597,6 +12791,7 @@ async function closeSession(sessionName) {
|
|
|
12597
12791
|
stdio: "ignore"
|
|
12598
12792
|
});
|
|
12599
12793
|
const timer = setTimeout(() => {
|
|
12794
|
+
unanswered = true;
|
|
12600
12795
|
child.kill("SIGTERM");
|
|
12601
12796
|
}, CLOSE_TIMEOUT_MS);
|
|
12602
12797
|
const finish = () => {
|
|
@@ -12606,6 +12801,7 @@ async function closeSession(sessionName) {
|
|
|
12606
12801
|
child.on("error", finish);
|
|
12607
12802
|
child.on("exit", finish);
|
|
12608
12803
|
});
|
|
12804
|
+
if (unanswered) await killSessionDaemon(sessionName);
|
|
12609
12805
|
}
|
|
12610
12806
|
//#endregion
|
|
12611
12807
|
//#region src/cli/run-live.ts
|
|
@@ -17279,7 +17475,7 @@ function runValidationAction(action, sessionName, envOverrides = {}) {
|
|
|
17279
17475
|
};
|
|
17280
17476
|
}
|
|
17281
17477
|
let result = spawnAB(built);
|
|
17282
|
-
if (result.status !== 0 &&
|
|
17478
|
+
if (result.status !== 0 && result.wedged === true) result = spawnAB(built);
|
|
17283
17479
|
if (result.status === 0) return {
|
|
17284
17480
|
skipped: false,
|
|
17285
17481
|
ok: true,
|
|
@@ -17410,10 +17606,6 @@ function rescueLostSteps(actions, kept, dropped, opts) {
|
|
|
17410
17606
|
rescuedSteps
|
|
17411
17607
|
};
|
|
17412
17608
|
}
|
|
17413
|
-
/** Did this agent-browser invocation get SIGTERM'd by the ccqa hard-timeout watchdog? */
|
|
17414
|
-
function looksLikeHardTimeout(result) {
|
|
17415
|
-
return result.stderr.includes("agent-browser killed after hard timeout");
|
|
17416
|
-
}
|
|
17417
17609
|
/**
|
|
17418
17610
|
* Passive (read-only) actions whose only effect is observation. When a
|
|
17419
17611
|
* preceding action fails, dropping these too is the right move because
|
package/dist/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as sanitizeStepId, n as FAILURE_SOURCE, r as FAILURE_STEP_ID, t as EVIDENCE_DIR_ENV } from "../evidence-constants-C425F7ZG.mjs";
|
|
2
|
-
import { n as spawnAB, t as sleepSync } from "../spawn-ab-
|
|
2
|
+
import { n as spawnAB, t as sleepSync } from "../spawn-ab-Bm34WBui.mjs";
|
|
3
3
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
//#region src/runtime/test-helpers.ts
|
|
@@ -163,10 +163,12 @@ function spawnABOnce(args) {
|
|
|
163
163
|
stdio: "pipe",
|
|
164
164
|
timeout: PROCESS_HARD_TIMEOUT_MS
|
|
165
165
|
});
|
|
166
|
+
const wedged = result.error?.code === "ETIMEDOUT";
|
|
166
167
|
return {
|
|
167
168
|
status: result.status,
|
|
168
169
|
stdout: result.stdout?.toString() ?? "",
|
|
169
|
-
stderr: (result.stderr?.toString() ?? "") + (
|
|
170
|
+
stderr: (result.stderr?.toString() ?? "") + (wedged ? "\n[ccqa] agent-browser killed after hard timeout" : ""),
|
|
171
|
+
wedged
|
|
170
172
|
};
|
|
171
173
|
}
|
|
172
174
|
/**
|