cruo-agent 0.1.16 → 0.1.17
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/README.md +2 -1
- package/dist/VERSION +1 -1
- package/dist/cli.js +386 -60
- package/dist/index.js +33 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -110,8 +110,9 @@ given up on work nobody abandoned.
|
|
|
110
110
|
| `--worktree` | a checkout per card |
|
|
111
111
|
| `--allow <tools>` | what the harness may use |
|
|
112
112
|
| `--push` | publish the branch after a run that commits |
|
|
113
|
-
| `--harness-timeout <seconds>` | kill a run that wedges (default 3000 — 50 minutes
|
|
113
|
+
| `--harness-timeout <seconds>` | kill a run that wedges (default 3000 — 50 minutes). It used to have to stay under an hour, because that is how long a run's board session lasted; since 0.1.17 the session is renewed under a running card, so what this bounds now is spend, not credentials |
|
|
114
114
|
| `--max-attempts <n>` | give up on a card after n unproductive runs (default 3) |
|
|
115
|
+
| `--max-cost <dollars>` | stop spending on one card once it has cost this much, and hand it to a human with what it has. Unset by default. Read from what the harness itself reported, so it only bites where those figures exist |
|
|
115
116
|
|
|
116
117
|
The full list, and what each is for: <https://cruo.space/docs#agents>
|
|
117
118
|
|
package/dist/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.17
|
package/dist/cli.js
CHANGED
|
@@ -36803,6 +36803,7 @@ var init_dist6 = __esm({
|
|
|
36803
36803
|
});
|
|
36804
36804
|
|
|
36805
36805
|
// ../../packages/mcp-kit/dist/context.js
|
|
36806
|
+
import { readFileSync } from "node:fs";
|
|
36806
36807
|
function decodeJwtExpiry(token) {
|
|
36807
36808
|
const parts = token.split(".");
|
|
36808
36809
|
if (parts.length !== 3)
|
|
@@ -36981,19 +36982,34 @@ function createRemoteContextResolver(options2) {
|
|
|
36981
36982
|
return attempt;
|
|
36982
36983
|
};
|
|
36983
36984
|
}
|
|
36984
|
-
function
|
|
36985
|
-
const
|
|
36986
|
-
if (!
|
|
36985
|
+
function readSessionFile() {
|
|
36986
|
+
const path = process.env[DELEGATED_SESSION_FILE_ENV];
|
|
36987
|
+
if (!path || !path.trim())
|
|
36988
|
+
return null;
|
|
36989
|
+
let raw;
|
|
36990
|
+
try {
|
|
36991
|
+
raw = readFileSync(path.trim(), "utf8");
|
|
36992
|
+
} catch {
|
|
36993
|
+
return null;
|
|
36994
|
+
}
|
|
36995
|
+
return parseSession(raw, DELEGATED_SESSION_FILE_ENV, "lenient");
|
|
36996
|
+
}
|
|
36997
|
+
function parseSession(raw, source, mode) {
|
|
36998
|
+
if (!raw.trim())
|
|
36987
36999
|
return null;
|
|
36988
37000
|
let parsed;
|
|
36989
37001
|
try {
|
|
36990
37002
|
parsed = JSON.parse(raw);
|
|
36991
37003
|
} catch {
|
|
36992
|
-
|
|
37004
|
+
if (mode === "strict")
|
|
37005
|
+
throw new TokenRefused(`${source} is not valid JSON.`);
|
|
37006
|
+
return null;
|
|
36993
37007
|
}
|
|
36994
37008
|
const s = parsed;
|
|
36995
37009
|
if (!s.accessToken || !s.userId || !s.email || !s.workspaceId) {
|
|
36996
|
-
|
|
37010
|
+
if (mode === "strict")
|
|
37011
|
+
throw new TokenRefused(`${source} is missing fields.`);
|
|
37012
|
+
return null;
|
|
36997
37013
|
}
|
|
36998
37014
|
return {
|
|
36999
37015
|
accessToken: s.accessToken,
|
|
@@ -37003,6 +37019,15 @@ function readDelegatedSession() {
|
|
|
37003
37019
|
workspaceName: s.workspaceName ?? "workspace"
|
|
37004
37020
|
};
|
|
37005
37021
|
}
|
|
37022
|
+
function readDelegatedSession() {
|
|
37023
|
+
const fromFile = readSessionFile();
|
|
37024
|
+
if (fromFile)
|
|
37025
|
+
return fromFile;
|
|
37026
|
+
const raw = process.env[DELEGATED_SESSION_ENV];
|
|
37027
|
+
if (!raw)
|
|
37028
|
+
return null;
|
|
37029
|
+
return parseSession(raw, DELEGATED_SESSION_ENV, "strict");
|
|
37030
|
+
}
|
|
37006
37031
|
function contextFromSession(schema, session) {
|
|
37007
37032
|
return {
|
|
37008
37033
|
client: createUserClientFor(session.accessToken, schema),
|
|
@@ -37030,7 +37055,7 @@ async function resolve(options2) {
|
|
|
37030
37055
|
const principal = await authenticate({ config: config3, token, product, mintedAt });
|
|
37031
37056
|
return mintContext({ config: config3, principal, schema });
|
|
37032
37057
|
}
|
|
37033
|
-
var TokenRefused, ASSUMED_SESSION_LIFETIME_MS, sessionInFlight, MINT_ATTEMPTS, DELEGATED_SESSION_ENV;
|
|
37058
|
+
var TokenRefused, ASSUMED_SESSION_LIFETIME_MS, sessionInFlight, MINT_ATTEMPTS, DELEGATED_SESSION_ENV, DELEGATED_SESSION_FILE_ENV;
|
|
37034
37059
|
var init_context = __esm({
|
|
37035
37060
|
"../../packages/mcp-kit/dist/context.js"() {
|
|
37036
37061
|
"use strict";
|
|
@@ -37046,6 +37071,7 @@ var init_context = __esm({
|
|
|
37046
37071
|
sessionInFlight = /* @__PURE__ */ new Map();
|
|
37047
37072
|
MINT_ATTEMPTS = 3;
|
|
37048
37073
|
DELEGATED_SESSION_ENV = "CRUO_SESSION";
|
|
37074
|
+
DELEGATED_SESSION_FILE_ENV = "CRUO_SESSION_FILE";
|
|
37049
37075
|
}
|
|
37050
37076
|
});
|
|
37051
37077
|
|
|
@@ -46048,6 +46074,7 @@ var init_options = __esm({
|
|
|
46048
46074
|
"interval",
|
|
46049
46075
|
"limit",
|
|
46050
46076
|
"max-attempts",
|
|
46077
|
+
"max-cost",
|
|
46051
46078
|
"max-interval",
|
|
46052
46079
|
"model",
|
|
46053
46080
|
"prepare",
|
|
@@ -46082,14 +46109,24 @@ function safeName(agent) {
|
|
|
46082
46109
|
const cleaned = agent.toLowerCase().replace(/[^a-z0-9._-]/g, "").replace(/^\.+/, "");
|
|
46083
46110
|
return cleaned === "" ? "default" : cleaned;
|
|
46084
46111
|
}
|
|
46112
|
+
function isCanonicalProcStart(value) {
|
|
46113
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value);
|
|
46114
|
+
}
|
|
46085
46115
|
async function processStartedAt(pid) {
|
|
46116
|
+
let raw;
|
|
46086
46117
|
try {
|
|
46087
|
-
const { stdout } = await exec("ps", ["-p", String(pid), "-o", "lstart="]);
|
|
46088
|
-
|
|
46089
|
-
return value === "" ? null : value;
|
|
46118
|
+
const { stdout } = await exec("ps", ["-p", String(pid), "-o", "lstart="], { env: PS_ENV });
|
|
46119
|
+
raw = stdout.trim();
|
|
46090
46120
|
} catch {
|
|
46091
46121
|
return null;
|
|
46092
46122
|
}
|
|
46123
|
+
if (raw === "") return null;
|
|
46124
|
+
const m = LSTART_PATTERN.exec(raw);
|
|
46125
|
+
if (!m) return null;
|
|
46126
|
+
const [, mon, day, hour, min, sec, year] = m;
|
|
46127
|
+
return new Date(
|
|
46128
|
+
Date.UTC(Number(year), LSTART_MONTHS.indexOf(mon), Number(day), Number(hour), Number(min), Number(sec))
|
|
46129
|
+
).toISOString();
|
|
46093
46130
|
}
|
|
46094
46131
|
function isAlive(pid) {
|
|
46095
46132
|
try {
|
|
@@ -46116,6 +46153,12 @@ async function readRecord(agent) {
|
|
|
46116
46153
|
return null;
|
|
46117
46154
|
}
|
|
46118
46155
|
}
|
|
46156
|
+
async function looksLikeThisAgent(pid, agent) {
|
|
46157
|
+
const argv2 = await processArgv(pid);
|
|
46158
|
+
if (!argv2 || !entryPosition(argv2)) return false;
|
|
46159
|
+
const asName = asNameFrom(argv2);
|
|
46160
|
+
return asName === null || asName === agent;
|
|
46161
|
+
}
|
|
46119
46162
|
async function liveness(agent, tidy = false) {
|
|
46120
46163
|
const record2 = await readRecord(agent);
|
|
46121
46164
|
if (!record2) return { state: "absent" };
|
|
@@ -46124,8 +46167,15 @@ async function liveness(agent, tidy = false) {
|
|
|
46124
46167
|
return { state: "gone", record: record2 };
|
|
46125
46168
|
}
|
|
46126
46169
|
const now = await processStartedAt(record2.pid);
|
|
46170
|
+
if (record2.procStartedAt !== null && !isCanonicalProcStart(record2.procStartedAt)) {
|
|
46171
|
+
if (!await looksLikeThisAgent(record2.pid, record2.agent)) {
|
|
46172
|
+
return { state: "recycled", record: record2 };
|
|
46173
|
+
}
|
|
46174
|
+
const migrated = tidy && now !== null ? { ...record2, procStartedAt: now } : record2;
|
|
46175
|
+
if (migrated !== record2) await writeRecord(migrated);
|
|
46176
|
+
return { state: "running", record: migrated };
|
|
46177
|
+
}
|
|
46127
46178
|
if (record2.procStartedAt !== null && now !== null && now !== record2.procStartedAt) {
|
|
46128
|
-
if (tidy) await removeRecord(agent);
|
|
46129
46179
|
return { state: "recycled", record: record2 };
|
|
46130
46180
|
}
|
|
46131
46181
|
return { state: "running", record: record2 };
|
|
@@ -46246,7 +46296,7 @@ function since(iso) {
|
|
|
46246
46296
|
if (ms2 < 864e5) return `${Math.round(ms2 / 36e5)}h`;
|
|
46247
46297
|
return `${Math.round(ms2 / 864e5)}d`;
|
|
46248
46298
|
}
|
|
46249
|
-
var exec, runDir, logDir, recordPath, CLI_BIN_NAMES, SUPERVISOR_BIN_NAMES, OWN_PACKAGE_MARKERS, NON_LOOPING_CLI_COMMANDS, NODE_FLAGS_WITH_VALUE, SIGNALS;
|
|
46299
|
+
var exec, runDir, logDir, recordPath, PS_ENV, LSTART_PATTERN, LSTART_MONTHS, CLI_BIN_NAMES, SUPERVISOR_BIN_NAMES, OWN_PACKAGE_MARKERS, NON_LOOPING_CLI_COMMANDS, NODE_FLAGS_WITH_VALUE, SIGNALS;
|
|
46250
46300
|
var init_process = __esm({
|
|
46251
46301
|
"src/process.ts"() {
|
|
46252
46302
|
"use strict";
|
|
@@ -46255,6 +46305,9 @@ var init_process = __esm({
|
|
|
46255
46305
|
runDir = () => join(homedir(), ".cruo", "run");
|
|
46256
46306
|
logDir = () => join(homedir(), ".cruo", "logs");
|
|
46257
46307
|
recordPath = (agent) => join(runDir(), `${safeName(agent)}.json`);
|
|
46308
|
+
PS_ENV = { ...process.env, LC_ALL: "C", LANG: "C", TZ: "UTC" };
|
|
46309
|
+
LSTART_PATTERN = /^\w{3}\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})$/;
|
|
46310
|
+
LSTART_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
46258
46311
|
CLI_BIN_NAMES = /* @__PURE__ */ new Set(["cruo", "cruo-agent"]);
|
|
46259
46312
|
SUPERVISOR_BIN_NAMES = /* @__PURE__ */ new Set(["cruo-supervisor"]);
|
|
46260
46313
|
OWN_PACKAGE_MARKERS = ["@cruo/mcp", "cruo-agent", "apps/mcp", "packages/cli"];
|
|
@@ -46466,6 +46519,11 @@ predate cost recording (\`CRA-89\`, 2026-08-29).
|
|
|
46466
46519
|
` ${pad(nameOf(id), w)} ${rpad(String(a.runs), 5)} ${rpad(String(a.productive), 7)} ${rpad(String(a.neverRan), 5)} ${rpad(duration3(a.ms), 7)} ${rpad(compact(a.input + a.cacheRead), 8)} ${rpad(compact(a.output), 8)} ${rpad(openingAvg, 8)} ${rpad(a.cost > 0 ? `$${a.cost.toFixed(2)}` : "\u2014", 9)}`
|
|
46467
46520
|
);
|
|
46468
46521
|
}
|
|
46522
|
+
if (byAgent.size > 1) {
|
|
46523
|
+
console.log(
|
|
46524
|
+
` ${pad("\u2014 total \u2014", w)} ${rpad(String(total.runs), 5)} ${rpad(String(total.productive), 7)} ${rpad(String(total.neverRan), 5)} ${rpad(duration3(total.ms), 7)} ${rpad(compact(total.input + total.cacheRead), 8)} ${rpad(compact(total.output), 8)} ${rpad("\u2014", 8)} ${rpad(total.cost > 0 ? `$${total.cost.toFixed(2)}` : "\u2014", 9)}`
|
|
46525
|
+
);
|
|
46526
|
+
}
|
|
46469
46527
|
if (!opts.issue) {
|
|
46470
46528
|
const byIssue = /* @__PURE__ */ new Map();
|
|
46471
46529
|
for (const r of rows) {
|
|
@@ -46510,6 +46568,11 @@ predate cost recording (\`CRA-89\`, 2026-08-29).
|
|
|
46510
46568
|
${total.runs} run(s), ${total.productive} productive, ${total.neverRan} never reached a model.
|
|
46511
46569
|
${duration3(total.ms)} of wall clock, ${compact(total.bytes)} bytes of output.`
|
|
46512
46570
|
);
|
|
46571
|
+
if (total.cacheRead > 0 || total.input > 0) {
|
|
46572
|
+
console.log(
|
|
46573
|
+
` ${compact(total.cacheRead)} tokens read from cache, ${compact(total.input)} fresh input, ${compact(total.output)} written \u2014 the read figure is what the bill tracks.`
|
|
46574
|
+
);
|
|
46575
|
+
}
|
|
46513
46576
|
if (total.measured === 0) {
|
|
46514
46577
|
console.log(
|
|
46515
46578
|
`
|
|
@@ -46571,6 +46634,7 @@ var init_usage = __esm({
|
|
|
46571
46634
|
});
|
|
46572
46635
|
|
|
46573
46636
|
// src/session-renewal.ts
|
|
46637
|
+
import { rename, writeFile as writeFile2 } from "node:fs/promises";
|
|
46574
46638
|
async function withFreshSession(ctx, attempt, renewal) {
|
|
46575
46639
|
const first = await attempt();
|
|
46576
46640
|
if (!first.error || !isExpiredSessionError(first.error)) {
|
|
@@ -46600,12 +46664,23 @@ async function withFreshSession(ctx, attempt, renewal) {
|
|
|
46600
46664
|
return first;
|
|
46601
46665
|
}
|
|
46602
46666
|
}
|
|
46603
|
-
|
|
46667
|
+
function sessionRefreshDue(expiresAtMs, nowMs, marginMs = SESSION_REFRESH_MARGIN_MS) {
|
|
46668
|
+
if (expiresAtMs === null) return true;
|
|
46669
|
+
return expiresAtMs - nowMs <= marginMs;
|
|
46670
|
+
}
|
|
46671
|
+
async function writeSessionFile(path, session) {
|
|
46672
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
46673
|
+
await writeFile2(tmp, JSON.stringify(session), { mode: 384 });
|
|
46674
|
+
await rename(tmp, path);
|
|
46675
|
+
}
|
|
46676
|
+
var warnedThisOutage, SESSION_REFRESH_MARGIN_MS, SESSION_REFRESH_CHECK_MS;
|
|
46604
46677
|
var init_session_renewal = __esm({
|
|
46605
46678
|
"src/session-renewal.ts"() {
|
|
46606
46679
|
"use strict";
|
|
46607
46680
|
init_dist7();
|
|
46608
46681
|
warnedThisOutage = false;
|
|
46682
|
+
SESSION_REFRESH_MARGIN_MS = 10 * 6e4;
|
|
46683
|
+
SESSION_REFRESH_CHECK_MS = 6e4;
|
|
46609
46684
|
}
|
|
46610
46685
|
});
|
|
46611
46686
|
|
|
@@ -46864,14 +46939,50 @@ var init_wake_clock = __esm({
|
|
|
46864
46939
|
function blocksAgentPickup(state) {
|
|
46865
46940
|
return state.requires_human === true;
|
|
46866
46941
|
}
|
|
46942
|
+
function stillEligible(subject) {
|
|
46943
|
+
const { issue: issue2, state, project } = subject;
|
|
46944
|
+
if (!issue2) return quiet("no longer exists");
|
|
46945
|
+
if (!state || !project) return quiet("can no longer be read (state or project missing)");
|
|
46946
|
+
if (subject.onlyProject && project.key !== subject.onlyProject) {
|
|
46947
|
+
return quiet(`is not in --project ${subject.onlyProject}`);
|
|
46948
|
+
}
|
|
46949
|
+
if (blocksAgentPickup(state)) {
|
|
46950
|
+
return gate(`is in ${state.name}, which requires a human`);
|
|
46951
|
+
}
|
|
46952
|
+
if (subject.reason === "assigned") {
|
|
46953
|
+
if (issue2.assignee_id !== subject.agentId) {
|
|
46954
|
+
return quiet("is no longer assigned to this agent");
|
|
46955
|
+
}
|
|
46956
|
+
if (state.category === "completed" || state.category === "canceled") {
|
|
46957
|
+
return quiet(`is in ${state.name}, which is ${state.category}`);
|
|
46958
|
+
}
|
|
46959
|
+
const said = subject.lastSaidAt;
|
|
46960
|
+
if (said && issue2.updated_at && said > issue2.updated_at) {
|
|
46961
|
+
return quiet("has not changed since this agent last commented");
|
|
46962
|
+
}
|
|
46963
|
+
return { ok: true };
|
|
46964
|
+
}
|
|
46965
|
+
if (subject.reason === "state") {
|
|
46966
|
+
if (state.owner_function !== subject.jobFunction) {
|
|
46967
|
+
return quiet(`is in ${state.name}, which "${subject.jobFunction}" does not own`);
|
|
46968
|
+
}
|
|
46969
|
+
if (issue2.assignee_id !== null && issue2.assignee_id !== subject.agentId) {
|
|
46970
|
+
return quiet("is now assigned to someone else");
|
|
46971
|
+
}
|
|
46972
|
+
return { ok: true };
|
|
46973
|
+
}
|
|
46974
|
+
return { ok: true };
|
|
46975
|
+
}
|
|
46867
46976
|
function byPriority(a, b) {
|
|
46868
46977
|
return (PRIORITY_RANK[a.issue.priority] ?? 9) - (PRIORITY_RANK[b.issue.priority] ?? 9);
|
|
46869
46978
|
}
|
|
46870
|
-
var PRIORITY_RANK;
|
|
46979
|
+
var gate, quiet, PRIORITY_RANK;
|
|
46871
46980
|
var init_queries = __esm({
|
|
46872
46981
|
"src/queries.ts"() {
|
|
46873
46982
|
"use strict";
|
|
46874
46983
|
init_dist();
|
|
46984
|
+
gate = (why) => ({ ok: false, why, announce: true });
|
|
46985
|
+
quiet = (why) => ({ ok: false, why, announce: false });
|
|
46875
46986
|
PRIORITY_RANK = {
|
|
46876
46987
|
urgent: 0,
|
|
46877
46988
|
high: 1,
|
|
@@ -46960,7 +47071,7 @@ async function createWorktree(ref, config3) {
|
|
|
46960
47071
|
if (staged.length === 0) return null;
|
|
46961
47072
|
const message = `wip(${ref}): saved by the supervisor when the run was cut short
|
|
46962
47073
|
|
|
46963
|
-
The
|
|
47074
|
+
The run ended \u2014 ${reason} \u2014 with this in the tree. NOT a handoff: the
|
|
46964
47075
|
agent did not choose to commit it and nothing here has been reviewed.
|
|
46965
47076
|
The next attempt on this card starts from here rather than from scratch.
|
|
46966
47077
|
|
|
@@ -47024,7 +47135,7 @@ var init_worktree = __esm({
|
|
|
47024
47135
|
var supervisor_exports = {};
|
|
47025
47136
|
import { spawn } from "node:child_process";
|
|
47026
47137
|
import { createRequire } from "node:module";
|
|
47027
|
-
import { mkdtemp, readdir as readdir3, readFile as readFile2, rm as rm3, stat as stat2, writeFile as
|
|
47138
|
+
import { mkdtemp, readdir as readdir3, readFile as readFile2, rm as rm3, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
|
|
47028
47139
|
import { homedir as homedir2, tmpdir } from "node:os";
|
|
47029
47140
|
import { join as join3 } from "node:path";
|
|
47030
47141
|
import { fileURLToPath } from "node:url";
|
|
@@ -47053,7 +47164,7 @@ function resolveWorktreeRoot(identity) {
|
|
|
47053
47164
|
const name = slug(identity.name) || slug(identity.email.split("@")[0] ?? "") || "agent";
|
|
47054
47165
|
return join3(homedir2(), ".cruo-work", name);
|
|
47055
47166
|
}
|
|
47056
|
-
async function pollAssigned(ctx) {
|
|
47167
|
+
async function pollAssigned(ctx, identity) {
|
|
47057
47168
|
const { data: issues, error: error51 } = await ctx.client.from("issues").select("*").eq("assignee_id", ctx.userId).order("created_at", { ascending: true });
|
|
47058
47169
|
if (error51) throw new Error(`Assignment poll failed: ${error51.message}`);
|
|
47059
47170
|
if (!issues || issues.length === 0) return [];
|
|
@@ -47072,17 +47183,23 @@ async function pollAssigned(ctx) {
|
|
|
47072
47183
|
return issues.flatMap((issue2) => {
|
|
47073
47184
|
const state = byState.get(issue2.status_id);
|
|
47074
47185
|
const project = byProject.get(issue2.project_id);
|
|
47075
|
-
|
|
47076
|
-
|
|
47077
|
-
|
|
47078
|
-
|
|
47079
|
-
|
|
47080
|
-
|
|
47186
|
+
const eligible = stillEligible({
|
|
47187
|
+
reason: "assigned",
|
|
47188
|
+
issue: issue2,
|
|
47189
|
+
state: state ?? null,
|
|
47190
|
+
project: project ?? null,
|
|
47191
|
+
agentId: ctx.userId,
|
|
47192
|
+
jobFunction: identity.jobFunction,
|
|
47193
|
+
onlyProject: options.project ?? null,
|
|
47194
|
+
lastSaidAt: lastSaid.get(issue2.id) ?? null
|
|
47195
|
+
});
|
|
47196
|
+
if (!eligible.ok) {
|
|
47197
|
+
if (eligible.announce && project) {
|
|
47198
|
+
log(` ${project.key}-${issue2.number} ${eligible.why} \u2014 not starting`);
|
|
47199
|
+
}
|
|
47081
47200
|
return [];
|
|
47082
47201
|
}
|
|
47083
|
-
if (
|
|
47084
|
-
const said = lastSaid.get(issue2.id);
|
|
47085
|
-
if (said && issue2.updated_at && said > issue2.updated_at) return [];
|
|
47202
|
+
if (!project || !state) return [];
|
|
47086
47203
|
return [
|
|
47087
47204
|
{
|
|
47088
47205
|
ref: `${project.key}-${issue2.number}`,
|
|
@@ -47112,14 +47229,22 @@ async function poll(ctx, identity) {
|
|
|
47112
47229
|
return issues.flatMap((issue2) => {
|
|
47113
47230
|
const project = byProject.get(issue2.project_id);
|
|
47114
47231
|
const state = byState.get(issue2.status_id);
|
|
47115
|
-
|
|
47116
|
-
|
|
47117
|
-
|
|
47118
|
-
|
|
47119
|
-
|
|
47120
|
-
|
|
47232
|
+
const eligible = stillEligible({
|
|
47233
|
+
reason: "state",
|
|
47234
|
+
issue: issue2,
|
|
47235
|
+
state: state ?? null,
|
|
47236
|
+
project: project ?? null,
|
|
47237
|
+
agentId: ctx.userId,
|
|
47238
|
+
jobFunction: identity.jobFunction,
|
|
47239
|
+
onlyProject: options.project ?? null
|
|
47240
|
+
});
|
|
47241
|
+
if (!eligible.ok) {
|
|
47242
|
+
if (eligible.announce && project) {
|
|
47243
|
+
log(` ${project.key}-${issue2.number} ${eligible.why} \u2014 not starting`);
|
|
47244
|
+
}
|
|
47121
47245
|
return [];
|
|
47122
47246
|
}
|
|
47247
|
+
if (!project || !state) return [];
|
|
47123
47248
|
return [
|
|
47124
47249
|
{
|
|
47125
47250
|
ref: `${project.key}-${issue2.number}`,
|
|
@@ -47161,8 +47286,22 @@ async function pollMentions(ctx, identity) {
|
|
|
47161
47286
|
return issues.flatMap((issue2) => {
|
|
47162
47287
|
const project = byProject.get(issue2.project_id);
|
|
47163
47288
|
const state = byState.get(issue2.status_id);
|
|
47289
|
+
const eligible = stillEligible({
|
|
47290
|
+
reason: "mention",
|
|
47291
|
+
issue: issue2,
|
|
47292
|
+
state: state ?? null,
|
|
47293
|
+
project: project ?? null,
|
|
47294
|
+
agentId: ctx.userId,
|
|
47295
|
+
jobFunction: identity.jobFunction,
|
|
47296
|
+
onlyProject: options.project ?? null
|
|
47297
|
+
});
|
|
47298
|
+
if (!eligible.ok) {
|
|
47299
|
+
if (eligible.announce && project) {
|
|
47300
|
+
log(` ${project.key}-${issue2.number} ${eligible.why} \u2014 not starting`);
|
|
47301
|
+
}
|
|
47302
|
+
return [];
|
|
47303
|
+
}
|
|
47164
47304
|
if (!project || !state) return [];
|
|
47165
|
-
if (options.project && project.key !== options.project) return [];
|
|
47166
47305
|
return [
|
|
47167
47306
|
{
|
|
47168
47307
|
ref: `${project.key}-${issue2.number}`,
|
|
@@ -47175,6 +47314,34 @@ async function pollMentions(ctx, identity) {
|
|
|
47175
47314
|
];
|
|
47176
47315
|
}).sort(byPriority);
|
|
47177
47316
|
}
|
|
47317
|
+
async function recheckEligibility(ctx, hit, identity) {
|
|
47318
|
+
const { data: issue2, error: error51 } = await ctx.client.from("issues").select("*").eq("id", hit.issue.id).maybeSingle();
|
|
47319
|
+
if (error51) {
|
|
47320
|
+
log(` ${hit.ref} could not be re-read (${error51.message}) \u2014 going on what the poll saw`);
|
|
47321
|
+
return { ok: true, hit };
|
|
47322
|
+
}
|
|
47323
|
+
if (!issue2) return { ok: false, why: "no longer exists" };
|
|
47324
|
+
const [{ data: state }, { data: said }] = await Promise.all([
|
|
47325
|
+
ctx.client.from("workflow_states").select("*").eq("id", issue2.status_id).maybeSingle(),
|
|
47326
|
+
// Only the `assigned` route reads this, and only to apply CRA-96's
|
|
47327
|
+
// already-answered rule; one query rather than a branch, because the cost
|
|
47328
|
+
// is a single indexed read next to a harness invocation.
|
|
47329
|
+
ctx.client.from("comments").select("created_at").eq("issue_id", issue2.id).eq("author_id", ctx.userId).order("created_at", { ascending: false }).limit(1).maybeSingle()
|
|
47330
|
+
]);
|
|
47331
|
+
const eligible = stillEligible({
|
|
47332
|
+
reason: hit.reason,
|
|
47333
|
+
issue: issue2,
|
|
47334
|
+
state: state ?? null,
|
|
47335
|
+
project: hit.project,
|
|
47336
|
+
agentId: ctx.userId,
|
|
47337
|
+
jobFunction: identity.jobFunction,
|
|
47338
|
+
onlyProject: options.project ?? null,
|
|
47339
|
+
lastSaidAt: said?.created_at ?? null
|
|
47340
|
+
});
|
|
47341
|
+
if (!eligible.ok) return { ok: false, why: eligible.why };
|
|
47342
|
+
if (!state) return { ok: false, why: "can no longer be read (state missing)" };
|
|
47343
|
+
return { ok: true, hit: { ...hit, issue: issue2, state } };
|
|
47344
|
+
}
|
|
47178
47345
|
async function claim(ctx, hit) {
|
|
47179
47346
|
const ttlMs = options.harnessTimeoutMs + 3e4;
|
|
47180
47347
|
const { data, error: error51 } = await withFreshSession(
|
|
@@ -47221,11 +47388,14 @@ async function writeMcpConfig() {
|
|
|
47221
47388
|
const session = await childSession();
|
|
47222
47389
|
const dir = await mkdtemp(join3(tmpdir(), CONFIG_DIR_PREFIX));
|
|
47223
47390
|
const path = join3(dir, "mcp.json");
|
|
47391
|
+
const sessionPath = join3(dir, "session.json");
|
|
47392
|
+
const writeSession = (s) => writeSessionFile(sessionPath, s);
|
|
47393
|
+
await writeSession(session);
|
|
47224
47394
|
const here = fileURLToPath(new URL(".", import.meta.url));
|
|
47225
47395
|
const bundled = /[/\\]dist[/\\]?$/.test(here);
|
|
47226
47396
|
const server = join3(here, bundled ? "index.js" : "index.ts");
|
|
47227
47397
|
const loader = bundled ? null : createRequire(import.meta.url).resolve("tsx");
|
|
47228
|
-
await
|
|
47398
|
+
await writeFile3(
|
|
47229
47399
|
path,
|
|
47230
47400
|
JSON.stringify(
|
|
47231
47401
|
{
|
|
@@ -47239,7 +47409,12 @@ async function writeMcpConfig() {
|
|
|
47239
47409
|
// Neither the service-role key nor the PAT. The child gets a
|
|
47240
47410
|
// session already minted from that PAT, by this process, which
|
|
47241
47411
|
// is the one entitled to hold the key that mints one.
|
|
47242
|
-
|
|
47412
|
+
// Both, deliberately (CRA-128). The file is what a renewal can
|
|
47413
|
+
// replace; the env var is the floor, so a child whose file is
|
|
47414
|
+
// unreadable for any reason behaves exactly as it did before
|
|
47415
|
+
// this existed rather than losing the board outright.
|
|
47416
|
+
[DELEGATED_SESSION_ENV]: JSON.stringify(session),
|
|
47417
|
+
[DELEGATED_SESSION_FILE_ENV]: sessionPath
|
|
47243
47418
|
}
|
|
47244
47419
|
}
|
|
47245
47420
|
}
|
|
@@ -47249,7 +47424,42 @@ async function writeMcpConfig() {
|
|
|
47249
47424
|
),
|
|
47250
47425
|
{ mode: 384 }
|
|
47251
47426
|
);
|
|
47252
|
-
|
|
47427
|
+
const keepSessionFresh = () => {
|
|
47428
|
+
let current = session;
|
|
47429
|
+
let stopped = false;
|
|
47430
|
+
let timer = null;
|
|
47431
|
+
const tick2 = async () => {
|
|
47432
|
+
if (stopped) return;
|
|
47433
|
+
if (sessionRefreshDue(decodeJwtExpiry(current.accessToken), Date.now())) {
|
|
47434
|
+
try {
|
|
47435
|
+
const fresh = await childSession();
|
|
47436
|
+
if (stopped) return;
|
|
47437
|
+
await writeSession(fresh);
|
|
47438
|
+
current = fresh;
|
|
47439
|
+
log(` session for this run renewed before expiry`);
|
|
47440
|
+
} catch (error51) {
|
|
47441
|
+
log(
|
|
47442
|
+
` could not renew this run's session (${error51 instanceof Error ? error51.message.split("\n")[0] : String(error51)}) \u2014 the harness keeps the one it has and this is retried`
|
|
47443
|
+
);
|
|
47444
|
+
}
|
|
47445
|
+
}
|
|
47446
|
+
if (!stopped) schedule();
|
|
47447
|
+
};
|
|
47448
|
+
const schedule = () => {
|
|
47449
|
+
timer = setTimeout(() => void tick2(), SESSION_REFRESH_CHECK_MS);
|
|
47450
|
+
timer.unref?.();
|
|
47451
|
+
};
|
|
47452
|
+
schedule();
|
|
47453
|
+
return () => {
|
|
47454
|
+
stopped = true;
|
|
47455
|
+
if (timer) clearTimeout(timer);
|
|
47456
|
+
};
|
|
47457
|
+
};
|
|
47458
|
+
return {
|
|
47459
|
+
path,
|
|
47460
|
+
keepSessionFresh,
|
|
47461
|
+
cleanup: () => rm3(dir, { recursive: true, force: true })
|
|
47462
|
+
};
|
|
47253
47463
|
}
|
|
47254
47464
|
async function sweepStaleConfigs() {
|
|
47255
47465
|
const dir = tmpdir();
|
|
@@ -47422,7 +47632,8 @@ function headFor(head2) {
|
|
|
47422
47632
|
}
|
|
47423
47633
|
async function invokeHarness(ctx, identity, hit, worktree) {
|
|
47424
47634
|
const cwd = worktree?.path ?? options.cwd;
|
|
47425
|
-
const { path: mcpConfig, cleanup } = await writeMcpConfig();
|
|
47635
|
+
const { path: mcpConfig, cleanup, keepSessionFresh } = await writeMcpConfig();
|
|
47636
|
+
const stopSessionRefresh = keepSessionFresh();
|
|
47426
47637
|
const streaming = options.usage === true;
|
|
47427
47638
|
const builtinTools = [
|
|
47428
47639
|
...new Set(
|
|
@@ -47562,6 +47773,7 @@ async function invokeHarness(ctx, identity, hit, worktree) {
|
|
|
47562
47773
|
});
|
|
47563
47774
|
});
|
|
47564
47775
|
} finally {
|
|
47776
|
+
stopSessionRefresh();
|
|
47565
47777
|
await cleanup();
|
|
47566
47778
|
}
|
|
47567
47779
|
}
|
|
@@ -47576,6 +47788,19 @@ async function loadAttempts(ctx, hits) {
|
|
|
47576
47788
|
for (const row of data ?? []) out.set(`${row.reason}:${row.issue_id}`, row.attempts);
|
|
47577
47789
|
return out;
|
|
47578
47790
|
}
|
|
47791
|
+
async function loadOutcomes(ctx, hits) {
|
|
47792
|
+
const out = /* @__PURE__ */ new Map();
|
|
47793
|
+
if (hits.length === 0) return out;
|
|
47794
|
+
const { data, error: error51 } = await ctx.client.from("issue_attempts").select("issue_id, reason, last_outcome").eq("agent_id", ctx.userId).in("issue_id", [...new Set(hits.map((h) => h.issue.id))]);
|
|
47795
|
+
if (error51) {
|
|
47796
|
+
log(` could not read why (${error51.message}) \u2014 logging without a reason`);
|
|
47797
|
+
return out;
|
|
47798
|
+
}
|
|
47799
|
+
for (const row of data ?? []) {
|
|
47800
|
+
if (row.last_outcome) out.set(`${row.reason}:${row.issue_id}`, row.last_outcome);
|
|
47801
|
+
}
|
|
47802
|
+
return out;
|
|
47803
|
+
}
|
|
47579
47804
|
async function recordAttempt(ctx, hit, outcome, previous) {
|
|
47580
47805
|
const attempts = previous + 1;
|
|
47581
47806
|
const { error: error51 } = await withFreshSession(
|
|
@@ -47599,6 +47824,47 @@ async function recordAttempt(ctx, hit, outcome, previous) {
|
|
|
47599
47824
|
if (error51) log(` could not record the attempt on ${hit.ref}: ${error51.message}`);
|
|
47600
47825
|
return attempts;
|
|
47601
47826
|
}
|
|
47827
|
+
async function loadSpend(ctx, hits) {
|
|
47828
|
+
const out = /* @__PURE__ */ new Map();
|
|
47829
|
+
if (hits.length === 0 || options.maxCostUsd === void 0) return out;
|
|
47830
|
+
const { data, error: error51 } = await ctx.client.from("harness_runs").select("issue_id, cost_usd").eq("agent_id", ctx.userId).in("issue_id", [...new Set(hits.map((h) => h.issue.id))]);
|
|
47831
|
+
if (error51) {
|
|
47832
|
+
log(` could not read spend so far (${error51.message}) \u2014 treating as $0`);
|
|
47833
|
+
return out;
|
|
47834
|
+
}
|
|
47835
|
+
for (const row of data ?? []) {
|
|
47836
|
+
if (row.cost_usd === null || row.issue_id === null) continue;
|
|
47837
|
+
out.set(row.issue_id, (out.get(row.issue_id) ?? 0) + Number(row.cost_usd));
|
|
47838
|
+
}
|
|
47839
|
+
return out;
|
|
47840
|
+
}
|
|
47841
|
+
async function handOffForCost(ctx, hit, spent) {
|
|
47842
|
+
const cap = options.maxCostUsd;
|
|
47843
|
+
const { error: commentError } = await ctx.client.from("comments").insert({
|
|
47844
|
+
issue_id: hit.issue.id,
|
|
47845
|
+
author_id: ctx.userId,
|
|
47846
|
+
body: `Spend ceiling reached: this card has cost **$${spent.toFixed(2)}**, at or over its \`--max-cost $${cap.toFixed(2)}\` limit, and is not finished. Stopping here rather than spending more on another run \u2014 a human should pick it up from whatever the runs so far left on it (state, comments, and any branch named on the card).`
|
|
47847
|
+
});
|
|
47848
|
+
if (commentError) {
|
|
47849
|
+
log(` ${hit.ref} could not post the spend-ceiling comment: ${commentError.message}`);
|
|
47850
|
+
}
|
|
47851
|
+
const { error: error51 } = await ctx.client.from("issue_attempts").upsert(
|
|
47852
|
+
{
|
|
47853
|
+
issue_id: hit.issue.id,
|
|
47854
|
+
agent_id: ctx.userId,
|
|
47855
|
+
reason: hit.reason,
|
|
47856
|
+
attempts: options.maxAttempts,
|
|
47857
|
+
last_tried_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
47858
|
+
last_outcome: `spend ceiling reached ($${spent.toFixed(2)} \u2265 $${cap.toFixed(2)})`,
|
|
47859
|
+
set_aside_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
47860
|
+
},
|
|
47861
|
+
{ onConflict: "issue_id,agent_id,reason" }
|
|
47862
|
+
);
|
|
47863
|
+
if (error51) log(` could not set aside ${hit.ref} for spend: ${error51.message}`);
|
|
47864
|
+
log(
|
|
47865
|
+
` ${hit.ref} hit its $${cap.toFixed(2)} spend ceiling ($${spent.toFixed(2)} spent) \u2014 commented and handed to a human, not invoking again`
|
|
47866
|
+
);
|
|
47867
|
+
}
|
|
47602
47868
|
async function clearAttempts(ctx, hit) {
|
|
47603
47869
|
const { error: error51 } = await withFreshSession(
|
|
47604
47870
|
ctx,
|
|
@@ -47715,7 +47981,7 @@ async function recordRun(ctx, hit, run, outcome) {
|
|
|
47715
47981
|
}
|
|
47716
47982
|
async function tick(ctx, identity, deadTicks, allowance) {
|
|
47717
47983
|
const mentions = await pollMentions(ctx, identity);
|
|
47718
|
-
const [assigned, stateWork] = await Promise.all([pollAssigned(ctx), poll(ctx, identity)]);
|
|
47984
|
+
const [assigned, stateWork] = await Promise.all([pollAssigned(ctx, identity), poll(ctx, identity)]);
|
|
47719
47985
|
const seen = /* @__PURE__ */ new Set();
|
|
47720
47986
|
const hits = [...mentions, ...assigned, ...stateWork].filter((h) => {
|
|
47721
47987
|
if (seen.has(h.ref)) return false;
|
|
@@ -47729,10 +47995,22 @@ async function tick(ctx, identity, deadTicks, allowance) {
|
|
|
47729
47995
|
}
|
|
47730
47996
|
const attemptsByHit = await loadAttempts(ctx, free);
|
|
47731
47997
|
const attemptsFor = (h) => attemptsByHit.get(`${h.reason}:${h.issue.id}`) ?? 0;
|
|
47732
|
-
const
|
|
47998
|
+
const spendByHit = await loadSpend(ctx, free);
|
|
47999
|
+
const spendFor = (h) => spendByHit.get(h.issue.id) ?? 0;
|
|
48000
|
+
const overBudget = options.maxCostUsd !== void 0 ? free.filter((h) => attemptsFor(h) < options.maxAttempts && spendFor(h) >= options.maxCostUsd) : [];
|
|
48001
|
+
for (const hit of overBudget) await handOffForCost(ctx, hit, spendFor(hit));
|
|
48002
|
+
const actionable = free.filter(
|
|
48003
|
+
(h) => attemptsFor(h) < options.maxAttempts && (options.maxCostUsd === void 0 || spendFor(h) < options.maxCostUsd)
|
|
48004
|
+
);
|
|
47733
48005
|
if (actionable.length === 0) {
|
|
47734
|
-
if (free.length > 0)
|
|
47735
|
-
|
|
48006
|
+
if (free.length > 0) {
|
|
48007
|
+
log(`${free.length} issue(s) waiting, all set aside`);
|
|
48008
|
+
const outcomes = await loadOutcomes(ctx, free);
|
|
48009
|
+
for (const h of free) {
|
|
48010
|
+
const why = outcomes.get(`${h.reason}:${h.issue.id}`);
|
|
48011
|
+
log(` ${h.ref}: ${why ?? "attempts exhausted, no reason recorded"}`);
|
|
48012
|
+
}
|
|
48013
|
+
} else if (heldByOthers.size > 0) log(`nothing free \u2014 everything waiting is claimed`);
|
|
47736
48014
|
await beat(ctx, { holding: 0, deadTicks, intervalMs: options.intervalMs });
|
|
47737
48015
|
return { invoked: 0, failedToRun: 0, suppressed: false, waiting: 0 };
|
|
47738
48016
|
}
|
|
@@ -47767,7 +48045,13 @@ async function tick(ctx, identity, deadTicks, allowance) {
|
|
|
47767
48045
|
allowance === "probe" ? ` probing with ${taking.length} of ${actionable.length} \u2014 one card is enough to learn whether the harness works again` : ` taking ${taking.length} this tick (--limit ${options.limit})`
|
|
47768
48046
|
);
|
|
47769
48047
|
}
|
|
47770
|
-
for (const
|
|
48048
|
+
for (const queued of taking) {
|
|
48049
|
+
const rechecked = await recheckEligibility(ctx, queued, identity);
|
|
48050
|
+
if (!rechecked.ok) {
|
|
48051
|
+
log(` ${queued.ref} ${rechecked.why} \u2014 skipping`);
|
|
48052
|
+
continue;
|
|
48053
|
+
}
|
|
48054
|
+
const hit = rechecked.hit;
|
|
47771
48055
|
const why = hit.reason === "mention" ? "mentioned" : hit.reason === "assigned" ? `assigned \xB7 ${hit.state.name}` : hit.state.name;
|
|
47772
48056
|
log(`\u2192 ${hit.ref} "${hit.issue.title}" (${why})`);
|
|
47773
48057
|
if (options.dryRun) {
|
|
@@ -47803,23 +48087,20 @@ async function tick(ctx, identity, deadTicks, allowance) {
|
|
|
47803
48087
|
try {
|
|
47804
48088
|
run = await invokeHarness(ctx, identity, hit, worktree);
|
|
47805
48089
|
if (worktree) committed = await worktree.commits();
|
|
47806
|
-
if (worktree
|
|
48090
|
+
if (worktree) {
|
|
48091
|
+
const endedBecause = run.killed ? "killed at the harness timeout" : run.refusal ? `stopped by a harness refusal (${run.refusal})` : run.code === 0 ? "exited 0 without committing this" : `cut short by exit ${run.code}`;
|
|
47807
48092
|
try {
|
|
47808
|
-
salvaged = await worktree.saveWip(
|
|
48093
|
+
salvaged = await worktree.saveWip(endedBecause);
|
|
47809
48094
|
if (salvaged) {
|
|
47810
48095
|
log(
|
|
47811
|
-
` ${hit.ref} killed with an uncommitted tree \u2014 saved as ${salvaged.slice(0, 8)} on ${worktree.branch}; the next attempt resumes from it`
|
|
48096
|
+
` ${hit.ref} ${run.killed ? "killed" : `exited ${run.code}`} with an uncommitted tree \u2014 saved as ${salvaged.slice(0, 8)} on ${worktree.branch}; the next attempt resumes from it`
|
|
47812
48097
|
);
|
|
47813
48098
|
}
|
|
47814
48099
|
} catch (error51) {
|
|
47815
48100
|
log(
|
|
47816
|
-
` ${hit.ref} could not save the
|
|
48101
|
+
` ${hit.ref} could not save the run's tree, and it goes with the worktree: ${error51 instanceof Error ? error51.message.split("\n")[0] : String(error51)}${options.keepFailed ? " (kept: --keep-failed)" : ""}`
|
|
47817
48102
|
);
|
|
47818
48103
|
}
|
|
47819
|
-
} else if (worktree && committed === 0 && await worktree.dirty()) {
|
|
47820
|
-
log(
|
|
47821
|
-
` ${hit.ref} exited ${run.code} leaving uncommitted changes \u2014 they go with the worktree${options.keepFailed ? " (kept: --keep-failed)" : "; use --keep-failed to inspect them"}`
|
|
47822
|
-
);
|
|
47823
48104
|
}
|
|
47824
48105
|
} finally {
|
|
47825
48106
|
stopBusyBeat();
|
|
@@ -48099,6 +48380,7 @@ var init_supervisor = __esm({
|
|
|
48099
48380
|
"src/supervisor.ts"() {
|
|
48100
48381
|
"use strict";
|
|
48101
48382
|
init_dist();
|
|
48383
|
+
init_session_renewal();
|
|
48102
48384
|
init_dist7();
|
|
48103
48385
|
init_env2();
|
|
48104
48386
|
init_auth();
|
|
@@ -48164,10 +48446,30 @@ var init_supervisor = __esm({
|
|
|
48164
48446
|
* test file — was killed at it twice, having written real work both times
|
|
48165
48447
|
* and committed neither. Err long: the cheap failure is the slow one.
|
|
48166
48448
|
*/
|
|
48167
|
-
// 50 minutes since CRA-122: roughly double the longest real run (26.3 min)
|
|
48168
|
-
//
|
|
48169
|
-
//
|
|
48170
|
-
//
|
|
48449
|
+
// 50 minutes since CRA-122: roughly double the longest real run (26.3 min).
|
|
48450
|
+
//
|
|
48451
|
+
// WHAT THE CEILING IS, AND WHY — read this before raising it (CRA-128 AC4).
|
|
48452
|
+
//
|
|
48453
|
+
// It used to be an hour, hard: the run's own board session lasted exactly 60
|
|
48454
|
+
// minutes and was captured by the harness at start-up, so a longer limit let
|
|
48455
|
+
// a run lose the board in the minutes it comments and hands the card on —
|
|
48456
|
+
// the one part of a run nobody else can reconstruct. **That is fixed.**
|
|
48457
|
+
// `writeMcpConfig` now writes the session to a file the child re-reads on
|
|
48458
|
+
// every tool call and rewrites it ten minutes ahead of expiry, so a run can
|
|
48459
|
+
// outlast its session.
|
|
48460
|
+
//
|
|
48461
|
+
// What still argues against a very long ceiling is money, not credentials.
|
|
48462
|
+
// `CRA-127` measured it: cost tracks cached tokens READ, almost linearly, and
|
|
48463
|
+
// a long run re-reads its own accumulating context on every turn — $8.04 for
|
|
48464
|
+
// 22 minutes at the top of the range. So `--harness-timeout` is also a spend
|
|
48465
|
+
// ceiling, and `--max-cost` is the one that says so out loud.
|
|
48466
|
+
//
|
|
48467
|
+
// One mechanical limit remains: `sweepStaleConfigs` removes a temp config
|
|
48468
|
+
// older than `max(--harness-timeout + --prepare-timeout + 5min, 1h)`, read
|
|
48469
|
+
// from the options of whichever supervisor is STARTING. Two supervisors with
|
|
48470
|
+
// very different timeouts, and the short one can in principle sweep the long
|
|
48471
|
+
// one's config mid-run. Nothing has hit this; raising the ceiling far past an
|
|
48472
|
+
// hour is what would make it reachable.
|
|
48171
48473
|
harnessTimeoutMs: ms("harness-timeout", 3e3),
|
|
48172
48474
|
/**
|
|
48173
48475
|
* Give up on an issue after this many unproductive runs. Persisted in
|
|
@@ -48175,6 +48477,24 @@ var init_supervisor = __esm({
|
|
|
48175
48477
|
* forgot every set-aside and nothing outside the process could see one.
|
|
48176
48478
|
*/
|
|
48177
48479
|
maxAttempts: num2("max-attempts", { fallback: 3, integer: true }),
|
|
48480
|
+
/**
|
|
48481
|
+
* Stop spending on ONE CARD once it has cost this many dollars, and hand it
|
|
48482
|
+
* to a human instead of trying again — `CRA-127`. Unset means no ceiling,
|
|
48483
|
+
* which is today's behaviour exactly: nothing stops a card costing $20.
|
|
48484
|
+
*
|
|
48485
|
+
* Read back from `harness_runs.cost_usd` — the same figure `cruo usage`
|
|
48486
|
+
* shows — rather than a separate counter, so the two can never disagree
|
|
48487
|
+
* about what a card has spent. That also means it only bites when the
|
|
48488
|
+
* harness is reporting cost at all (`--usage`, for Claude Code): a harness
|
|
48489
|
+
* that volunteers nothing is read as $0 spent, honestly, not metered
|
|
48490
|
+
* against a figure it never produced.
|
|
48491
|
+
*
|
|
48492
|
+
* Checked BEFORE a harness is spawned, not after — the whole finding on
|
|
48493
|
+
* this card is that a run's cost is paid turn by turn as it re-reads its
|
|
48494
|
+
* own context, so the only ceiling worth having is one that stops the next
|
|
48495
|
+
* invocation rather than one that bills it and complains afterwards.
|
|
48496
|
+
*/
|
|
48497
|
+
maxCostUsd: num2("max-cost"),
|
|
48178
48498
|
/**
|
|
48179
48499
|
* Ask the harness to report what each run spent — `CRA-89`.
|
|
48180
48500
|
*
|
|
@@ -48320,7 +48640,7 @@ init_dist();
|
|
|
48320
48640
|
init_dist7();
|
|
48321
48641
|
init_options();
|
|
48322
48642
|
init_process();
|
|
48323
|
-
import { chmod, mkdir as mkdir3, readFile as readFile3, rm as rm4, writeFile as
|
|
48643
|
+
import { chmod, mkdir as mkdir3, readFile as readFile3, rm as rm4, writeFile as writeFile4 } from "node:fs/promises";
|
|
48324
48644
|
import { homedir as homedir3 } from "node:os";
|
|
48325
48645
|
import { join as join4 } from "node:path";
|
|
48326
48646
|
var CONFIG_DIR = join4(homedir3(), ".cruo");
|
|
@@ -48398,7 +48718,7 @@ async function readConfig2() {
|
|
|
48398
48718
|
}
|
|
48399
48719
|
async function writeConfig(config3) {
|
|
48400
48720
|
await mkdir3(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
48401
|
-
await
|
|
48721
|
+
await writeFile4(CONFIG_PATH, JSON.stringify(config3, null, 2) + "\n", { mode: 384 });
|
|
48402
48722
|
await chmod(CONFIG_PATH, 384);
|
|
48403
48723
|
}
|
|
48404
48724
|
async function lookup(token, timeoutMs = 5e3) {
|
|
@@ -48614,9 +48934,14 @@ Not signalled. Look at it yourself, then act if you are sure:
|
|
|
48614
48934
|
${name} is not running (it left a record behind; cleared).
|
|
48615
48935
|
`);
|
|
48616
48936
|
} else if (live.state === "recycled") {
|
|
48617
|
-
console.error(
|
|
48618
|
-
|
|
48619
|
-
|
|
48937
|
+
console.error(
|
|
48938
|
+
`
|
|
48939
|
+
${name}'s record names pid ${live.record.pid}, which is alive but does not look like the same process anymore \u2014 cannot confirm it is still ${name}. Not touched: nothing here deletes the record of a pid that is alive. Look at it yourself, then act if you are sure:
|
|
48940
|
+
|
|
48941
|
+
ps -p ${live.record.pid} -o pid,lstart,command
|
|
48942
|
+
rm ~/.cruo/run/${safeName(name)}.json # only once you are sure it is not ${name}
|
|
48943
|
+
`
|
|
48944
|
+
);
|
|
48620
48945
|
} else {
|
|
48621
48946
|
console.error(`
|
|
48622
48947
|
${name} is not running. Start it with \`cruo start --as ${name}\`.
|
|
@@ -48736,8 +49061,9 @@ Nothing running. Start one with \`cruo start\`.
|
|
|
48736
49061
|
console.log(` ${agent.padEnd(14)} \u2014 \u2014 died or was killed (record cleared)`);
|
|
48737
49062
|
await removeRecord(agent);
|
|
48738
49063
|
} else if (live.state === "recycled") {
|
|
48739
|
-
console.log(
|
|
48740
|
-
|
|
49064
|
+
console.log(
|
|
49065
|
+
` ${agent.padEnd(14)} ${String(live.record.pid).padEnd(8)} \u2014 cannot confirm this is still the same process \u2014 left for you to check`
|
|
49066
|
+
);
|
|
48741
49067
|
}
|
|
48742
49068
|
}
|
|
48743
49069
|
for (const proc of untracked) {
|
package/dist/index.js
CHANGED
|
@@ -45048,7 +45048,7 @@ var require_dist_cjs13 = __commonJS({
|
|
|
45048
45048
|
"../../node_modules/.pnpm/@aws-sdk+credential-provider-web-identity@3.972.71/node_modules/@aws-sdk/credential-provider-web-identity/dist-cjs/index.js"(exports) {
|
|
45049
45049
|
var { setCredentialFeature: setCredentialFeature2 } = (init_client3(), __toCommonJS(client_exports2));
|
|
45050
45050
|
var { CredentialsProviderError: CredentialsProviderError2, externalDataInterceptor: externalDataInterceptor2 } = (init_config2(), __toCommonJS(config_exports));
|
|
45051
|
-
var { readFileSync } = __require("node:fs");
|
|
45051
|
+
var { readFileSync: readFileSync2 } = __require("node:fs");
|
|
45052
45052
|
var fromWebToken = (init) => async (awsIdentityProperties) => {
|
|
45053
45053
|
init.logger?.debug("@aws-sdk/credential-provider-web-identity - fromWebToken");
|
|
45054
45054
|
const { roleArn, roleSessionName, webIdentityToken, providerId, policyArns, policy, durationSeconds } = init;
|
|
@@ -45089,7 +45089,7 @@ var require_dist_cjs13 = __commonJS({
|
|
|
45089
45089
|
}
|
|
45090
45090
|
const credentials = await fromWebToken({
|
|
45091
45091
|
...init,
|
|
45092
|
-
webIdentityToken: externalDataInterceptor2?.getTokenRecord?.()[webIdentityTokenFile] ??
|
|
45092
|
+
webIdentityToken: externalDataInterceptor2?.getTokenRecord?.()[webIdentityTokenFile] ?? readFileSync2(webIdentityTokenFile, { encoding: "ascii" }),
|
|
45093
45093
|
roleArn,
|
|
45094
45094
|
roleSessionName
|
|
45095
45095
|
})(awsIdentityProperties);
|
|
@@ -78141,6 +78141,7 @@ async function touchAccessToken(admin, tokenId) {
|
|
|
78141
78141
|
}
|
|
78142
78142
|
|
|
78143
78143
|
// ../../packages/mcp-kit/dist/context.js
|
|
78144
|
+
import { readFileSync } from "node:fs";
|
|
78144
78145
|
function decodeJwtExpiry(token) {
|
|
78145
78146
|
const parts = token.split(".");
|
|
78146
78147
|
if (parts.length !== 3)
|
|
@@ -78255,6 +78256,7 @@ async function sessionFor(config3, principal) {
|
|
|
78255
78256
|
return attempt;
|
|
78256
78257
|
}
|
|
78257
78258
|
var DELEGATED_SESSION_ENV = "CRUO_SESSION";
|
|
78259
|
+
var DELEGATED_SESSION_FILE_ENV = "CRUO_SESSION_FILE";
|
|
78258
78260
|
async function fetchDelegatedSession(endpoint, token, options = {}) {
|
|
78259
78261
|
let response;
|
|
78260
78262
|
try {
|
|
@@ -78306,19 +78308,34 @@ function createRemoteContextResolver(options) {
|
|
|
78306
78308
|
return attempt;
|
|
78307
78309
|
};
|
|
78308
78310
|
}
|
|
78309
|
-
function
|
|
78310
|
-
const
|
|
78311
|
-
if (!
|
|
78311
|
+
function readSessionFile() {
|
|
78312
|
+
const path = process.env[DELEGATED_SESSION_FILE_ENV];
|
|
78313
|
+
if (!path || !path.trim())
|
|
78314
|
+
return null;
|
|
78315
|
+
let raw;
|
|
78316
|
+
try {
|
|
78317
|
+
raw = readFileSync(path.trim(), "utf8");
|
|
78318
|
+
} catch {
|
|
78319
|
+
return null;
|
|
78320
|
+
}
|
|
78321
|
+
return parseSession(raw, DELEGATED_SESSION_FILE_ENV, "lenient");
|
|
78322
|
+
}
|
|
78323
|
+
function parseSession(raw, source, mode) {
|
|
78324
|
+
if (!raw.trim())
|
|
78312
78325
|
return null;
|
|
78313
78326
|
let parsed;
|
|
78314
78327
|
try {
|
|
78315
78328
|
parsed = JSON.parse(raw);
|
|
78316
78329
|
} catch {
|
|
78317
|
-
|
|
78330
|
+
if (mode === "strict")
|
|
78331
|
+
throw new TokenRefused(`${source} is not valid JSON.`);
|
|
78332
|
+
return null;
|
|
78318
78333
|
}
|
|
78319
78334
|
const s2 = parsed;
|
|
78320
78335
|
if (!s2.accessToken || !s2.userId || !s2.email || !s2.workspaceId) {
|
|
78321
|
-
|
|
78336
|
+
if (mode === "strict")
|
|
78337
|
+
throw new TokenRefused(`${source} is missing fields.`);
|
|
78338
|
+
return null;
|
|
78322
78339
|
}
|
|
78323
78340
|
return {
|
|
78324
78341
|
accessToken: s2.accessToken,
|
|
@@ -78328,6 +78345,15 @@ function readDelegatedSession() {
|
|
|
78328
78345
|
workspaceName: s2.workspaceName ?? "workspace"
|
|
78329
78346
|
};
|
|
78330
78347
|
}
|
|
78348
|
+
function readDelegatedSession() {
|
|
78349
|
+
const fromFile = readSessionFile();
|
|
78350
|
+
if (fromFile)
|
|
78351
|
+
return fromFile;
|
|
78352
|
+
const raw = process.env[DELEGATED_SESSION_ENV];
|
|
78353
|
+
if (!raw)
|
|
78354
|
+
return null;
|
|
78355
|
+
return parseSession(raw, DELEGATED_SESSION_ENV, "strict");
|
|
78356
|
+
}
|
|
78331
78357
|
function contextFromSession(schema, session) {
|
|
78332
78358
|
return {
|
|
78333
78359
|
client: createUserClientFor(session.accessToken, schema),
|
package/package.json
CHANGED