@tablation/crew 0.94.0 → 0.95.0
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/crew.example.yaml +1 -0
- package/dist/cli.js +175 -15
- package/package.json +1 -1
package/crew.example.yaml
CHANGED
|
@@ -56,6 +56,7 @@ ship:
|
|
|
56
56
|
# deep: { model: claude-opus-5-5 }
|
|
57
57
|
# args: [] # appended to the harness argv
|
|
58
58
|
# env: {} # added to the session environment
|
|
59
|
+
# maxRunMinutes: 240 # wall-clock limit per agent run (default 240; 0 = none); a route's maxRunMinutes overrides it (CREW-2786)
|
|
59
60
|
# gemini: { harness: gemini, bin: gemini, model: gemini-2.5-pro }
|
|
60
61
|
|
|
61
62
|
# shell: bash # defaults per platform
|
package/dist/cli.js
CHANGED
|
@@ -2665,7 +2665,8 @@ var ROUTE_KEYS = /* @__PURE__ */ new Set([
|
|
|
2665
2665
|
"promptSet",
|
|
2666
2666
|
"hostPassengers",
|
|
2667
2667
|
"hostPeriscopes",
|
|
2668
|
-
"toolchain"
|
|
2668
|
+
"toolchain",
|
|
2669
|
+
"maxRunMinutes"
|
|
2669
2670
|
]);
|
|
2670
2671
|
function parseWorktreeName(v, where, file) {
|
|
2671
2672
|
const problem = typeof v === "string" ? worktreeNameProblem(v) : "must be a string";
|
|
@@ -3075,7 +3076,21 @@ Create the first of those:
|
|
|
3075
3076
|
function expandCommand(cmd, base) {
|
|
3076
3077
|
return cmd.includes("/") || cmd.startsWith("~") ? expand(cmd, base) : cmd;
|
|
3077
3078
|
}
|
|
3078
|
-
var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "launcherBin", "model", "tiers", "args", "env", "maxThinkingTokens", "auth"]);
|
|
3079
|
+
var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "launcherBin", "model", "tiers", "args", "env", "maxThinkingTokens", "auth", "maxRunMinutes"]);
|
|
3080
|
+
var MAX_RUN_MINUTES_CEILING = Math.floor((2 ** 31 - 1) / 6e4);
|
|
3081
|
+
function parseMaxRunMinutes(raw, where, missing) {
|
|
3082
|
+
if (raw === void 0) return void 0;
|
|
3083
|
+
const n = Number(raw);
|
|
3084
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
3085
|
+
missing.add(`${where} must be a non-negative integer number of minutes, 0 for no limit (got "${String(raw)}")`);
|
|
3086
|
+
return void 0;
|
|
3087
|
+
}
|
|
3088
|
+
if (n > MAX_RUN_MINUTES_CEILING) {
|
|
3089
|
+
missing.add(`${where} is too large (${n}); the most it can be is ${MAX_RUN_MINUTES_CEILING} minutes \u2014 use 0 for no limit`);
|
|
3090
|
+
return void 0;
|
|
3091
|
+
}
|
|
3092
|
+
return n;
|
|
3093
|
+
}
|
|
3079
3094
|
function parsePair(raw, missing) {
|
|
3080
3095
|
if (raw === void 0) return void 0;
|
|
3081
3096
|
if (!Array.isArray(raw)) {
|
|
@@ -3132,6 +3147,7 @@ function parseToolchains(raw, base, missing, file) {
|
|
|
3132
3147
|
if (!Number.isInteger(maxThinkingTokens) || maxThinkingTokens < 0) {
|
|
3133
3148
|
missing.add(`${where}.maxThinkingTokens must be a non-negative integer (got "${entryRaw.maxThinkingTokens}")`);
|
|
3134
3149
|
}
|
|
3150
|
+
const maxRunMinutes = parseMaxRunMinutes(entryRaw.maxRunMinutes, `${where}.maxRunMinutes`, missing);
|
|
3135
3151
|
let auth;
|
|
3136
3152
|
if (entryRaw.auth !== void 0) {
|
|
3137
3153
|
if (entryRaw.auth !== "subscription" && entryRaw.auth !== "inherit") {
|
|
@@ -3153,7 +3169,8 @@ function parseToolchains(raw, base, missing, file) {
|
|
|
3153
3169
|
tiers,
|
|
3154
3170
|
args: Array.isArray(args) ? args.map(String) : [],
|
|
3155
3171
|
env: Object.fromEntries(Object.entries(env).map(([k, v]) => [k, String(v)])),
|
|
3156
|
-
maxThinkingTokens
|
|
3172
|
+
maxThinkingTokens,
|
|
3173
|
+
...maxRunMinutes !== void 0 ? { maxRunMinutes } : {}
|
|
3157
3174
|
});
|
|
3158
3175
|
}
|
|
3159
3176
|
return out;
|
|
@@ -3161,6 +3178,7 @@ function parseToolchains(raw, base, missing, file) {
|
|
|
3161
3178
|
function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, toolchainNames) {
|
|
3162
3179
|
rejectUnknownKeys(c, ROUTE_KEYS, `${where}`, file);
|
|
3163
3180
|
const toolchain = c?.toolchain === void 0 ? void 0 : String(c.toolchain);
|
|
3181
|
+
const maxRunMinutes = parseMaxRunMinutes(c?.maxRunMinutes, `${where}.maxRunMinutes`, missing);
|
|
3164
3182
|
if (toolchain !== void 0 && !toolchainNames.has(toolchain)) {
|
|
3165
3183
|
missing.add(`${where}.toolchain "${toolchain}" is not defined in ship.toolchains (have: ${[...toolchainNames].join(", ")})`);
|
|
3166
3184
|
}
|
|
@@ -3224,6 +3242,7 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
|
|
|
3224
3242
|
repoOverrides,
|
|
3225
3243
|
worktreePrefix: c?.worktreePrefix ? String(c.worktreePrefix) : void 0,
|
|
3226
3244
|
...toolchain !== void 0 ? { toolchain } : {},
|
|
3245
|
+
...maxRunMinutes !== void 0 ? { maxRunMinutes } : {},
|
|
3227
3246
|
promptsDir: resolvePromptsDir(c?.promptSet ? String(c.promptSet) : void 0, crewHome, base),
|
|
3228
3247
|
weight,
|
|
3229
3248
|
baseUrl: String(c?.baseUrl ?? raw.ship?.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
|
|
@@ -7563,6 +7582,28 @@ var Tracker = class {
|
|
|
7563
7582
|
if (!auth.commentShip) missing.push(`Comments.${this.contract.comments.ship}`);
|
|
7564
7583
|
return missing;
|
|
7565
7584
|
}
|
|
7585
|
+
/**
|
|
7586
|
+
* The runtime-map columns (ISSUE-2791) this workspace's template has not
|
|
7587
|
+
* taken yet, as `table.column` names for `crew doctor`. Each is read as its
|
|
7588
|
+
* default when absent, so this is informational: `Repos.forge_host`
|
|
7589
|
+
* (default `github.com`), `Ships.kind` (`developer_machine`),
|
|
7590
|
+
* `Ships.instance_class` and `Ships.relay_url` (blank). A table the route
|
|
7591
|
+
* does not have is skipped, not reported.
|
|
7592
|
+
*/
|
|
7593
|
+
async missingRuntimeColumns() {
|
|
7594
|
+
const wanted = [
|
|
7595
|
+
[this.route.resolved?.reposModelId, "Repos", ["forge_host"]],
|
|
7596
|
+
[this.route.resolved?.shipsModelId, "Ships", ["kind", "instance_class", "relay_url"]]
|
|
7597
|
+
];
|
|
7598
|
+
const missing = [];
|
|
7599
|
+
for (const [modelId, label, columns] of wanted) {
|
|
7600
|
+
if (!modelId) continue;
|
|
7601
|
+
const have = await this.columnNames(modelId);
|
|
7602
|
+
if (have.size === 0) continue;
|
|
7603
|
+
for (const c of columns) if (!have.has(c)) missing.push(`${label}.${c}`);
|
|
7604
|
+
}
|
|
7605
|
+
return missing;
|
|
7606
|
+
}
|
|
7566
7607
|
/**
|
|
7567
7608
|
* The public `/v/<slug>/r/<token>` link for one ticket (ISSUE-928) —
|
|
7568
7609
|
* fetch-or-create via `POST /views/:viewId/record-links`, so a repeated
|
|
@@ -9190,6 +9231,55 @@ function skillMarkerWarnings(promptsDir, personaPrompts) {
|
|
|
9190
9231
|
return out;
|
|
9191
9232
|
}
|
|
9192
9233
|
|
|
9234
|
+
// src/run-limit.ts
|
|
9235
|
+
var DEFAULT_MAX_RUN_MINUTES = 240;
|
|
9236
|
+
var KILL_GRACE_MS = 5e3;
|
|
9237
|
+
var WALL_CLOCK_REASON = "wall-clock";
|
|
9238
|
+
function resolveMaxRunMinutes(route2, toolchain) {
|
|
9239
|
+
return route2 ?? toolchain ?? DEFAULT_MAX_RUN_MINUTES;
|
|
9240
|
+
}
|
|
9241
|
+
function deadlineFor(spawnedAt, maxRunMinutes) {
|
|
9242
|
+
if (!maxRunMinutes || maxRunMinutes <= 0) return void 0;
|
|
9243
|
+
const t = Date.parse(spawnedAt);
|
|
9244
|
+
return Number.isNaN(t) ? void 0 : new Date(t + maxRunMinutes * 6e4).toISOString();
|
|
9245
|
+
}
|
|
9246
|
+
function msUntilDeadline(deadline, now = Date.now()) {
|
|
9247
|
+
if (!deadline) return void 0;
|
|
9248
|
+
const t = Date.parse(deadline);
|
|
9249
|
+
return Number.isNaN(t) ? void 0 : t - now;
|
|
9250
|
+
}
|
|
9251
|
+
async function killRunGroup(pid, o = {}) {
|
|
9252
|
+
const alive = o.isAlive ?? ((p) => {
|
|
9253
|
+
try {
|
|
9254
|
+
process.kill(p, 0);
|
|
9255
|
+
return true;
|
|
9256
|
+
} catch (e) {
|
|
9257
|
+
return e.code === "EPERM";
|
|
9258
|
+
}
|
|
9259
|
+
});
|
|
9260
|
+
const signal = (sig) => {
|
|
9261
|
+
try {
|
|
9262
|
+
process.kill(-pid, sig);
|
|
9263
|
+
} catch {
|
|
9264
|
+
try {
|
|
9265
|
+
process.kill(pid, sig);
|
|
9266
|
+
} catch {
|
|
9267
|
+
}
|
|
9268
|
+
}
|
|
9269
|
+
};
|
|
9270
|
+
signal("SIGTERM");
|
|
9271
|
+
const end = Date.now() + (o.graceMs ?? KILL_GRACE_MS);
|
|
9272
|
+
while (alive(pid) && Date.now() < end) await new Promise((r) => setTimeout(r, 50));
|
|
9273
|
+
if (alive(pid)) signal("SIGKILL");
|
|
9274
|
+
}
|
|
9275
|
+
var RUN_KILLED_MARKER = "<!-- crew:run-killed -->";
|
|
9276
|
+
function runKilledComment(b) {
|
|
9277
|
+
const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
|
|
9278
|
+
return `${RUN_KILLED_MARKER}
|
|
9279
|
+
|
|
9280
|
+
The agent run on this ticket was stopped at its wall-clock limit of ${b.limitMinutes} minutes before it handed on${b.branch ? `; its branch is \`${b.branch}\`` : ""}. ${tree} The worktree is left as it was. The next run on this seat resumes from it rather than starting over.`;
|
|
9281
|
+
}
|
|
9282
|
+
|
|
9193
9283
|
// src/agent.ts
|
|
9194
9284
|
var DIGEST_MAX_AGE_SECONDS = 600;
|
|
9195
9285
|
var LEGACY_TOOLCHAIN_HARNESS = "claude";
|
|
@@ -9299,6 +9389,7 @@ function planAgentRun(o) {
|
|
|
9299
9389
|
toolchain: toolchain.name,
|
|
9300
9390
|
harness: toolchain.harness,
|
|
9301
9391
|
provider: toolchainProvider(toolchain),
|
|
9392
|
+
maxRunMinutes: resolveMaxRunMinutes(o.route.maxRunMinutes, toolchain.maxRunMinutes),
|
|
9302
9393
|
resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
|
|
9303
9394
|
warnings: [...seat.warnings ?? [], ...expanded.warnings],
|
|
9304
9395
|
prompt,
|
|
@@ -9388,6 +9479,17 @@ function identityOf(plan, adapterHarness) {
|
|
|
9388
9479
|
promptSha: plan.promptSha
|
|
9389
9480
|
};
|
|
9390
9481
|
}
|
|
9482
|
+
async function killForDeadline(pid, emit, role, ticket, spawnedAt, limitMinutes) {
|
|
9483
|
+
const elapsedMs = Date.now() - Date.parse(spawnedAt);
|
|
9484
|
+
const limit = limitMinutes ?? Math.round(elapsedMs / 6e4);
|
|
9485
|
+
emit.emit(`run_killed: ${role} run exceeded its wall-clock limit of ${limit} min (ran ${Math.round(elapsedMs / 6e4)} min)`, {
|
|
9486
|
+
role,
|
|
9487
|
+
...ticket ? { ticket } : {},
|
|
9488
|
+
data: { event: "run_killed", reason: WALL_CLOCK_REASON, limitMinutes: limit, elapsedMs, pid }
|
|
9489
|
+
});
|
|
9490
|
+
await killRunGroup(pid);
|
|
9491
|
+
return { limitMinutes: limit, elapsedMs };
|
|
9492
|
+
}
|
|
9391
9493
|
var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
|
|
9392
9494
|
function openSink(path) {
|
|
9393
9495
|
try {
|
|
@@ -9747,13 +9849,16 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9747
9849
|
detached: true
|
|
9748
9850
|
});
|
|
9749
9851
|
closeSync3(streamFd);
|
|
9852
|
+
const spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9853
|
+
const deadline = deadlineFor(spawnedAt, plan.maxRunMinutes);
|
|
9750
9854
|
if (child.pid !== void 0) {
|
|
9751
9855
|
const sidecar = {
|
|
9752
9856
|
pid: child.pid,
|
|
9753
9857
|
ownerPid: process.pid,
|
|
9754
|
-
spawnedAt
|
|
9858
|
+
spawnedAt,
|
|
9755
9859
|
role: plan.role,
|
|
9756
9860
|
route: plan.route,
|
|
9861
|
+
...deadline ? { deadline, maxRunMinutes: plan.maxRunMinutes } : {},
|
|
9757
9862
|
ticket: plan.ticket ?? null,
|
|
9758
9863
|
ticketRecordId: plan.ticketRecordId ?? null,
|
|
9759
9864
|
repo: plan.repo ?? null,
|
|
@@ -9773,25 +9878,45 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9773
9878
|
}
|
|
9774
9879
|
}
|
|
9775
9880
|
const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
|
|
9776
|
-
const meta = { pid: child.pid ?? null };
|
|
9881
|
+
const meta = { pid: child.pid ?? null, ...deadline ? { deadline } : {} };
|
|
9777
9882
|
obs.onClaim = () => writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
9778
9883
|
if (child.pid !== void 0) writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
9779
9884
|
const tail3 = new FileTail(plan.streamPath);
|
|
9780
9885
|
const timer = setInterval(() => obs.feed(tail3.poll()), TAIL_POLL_MS);
|
|
9886
|
+
let killed;
|
|
9887
|
+
const limitTimer = child.pid !== void 0 && deadline ? setTimeout(() => {
|
|
9888
|
+
const pid = child.pid;
|
|
9889
|
+
void killForDeadline(pid, emit, plan.role, obs.liveTicket ?? plan.ticket ?? null, spawnedAt, plan.maxRunMinutes).then((k) => {
|
|
9890
|
+
killed = k;
|
|
9891
|
+
});
|
|
9892
|
+
}, Math.max(0, msUntilDeadline(deadline) ?? 0)) : void 0;
|
|
9781
9893
|
let spawnFailed = false;
|
|
9782
9894
|
child.on("error", (err3) => {
|
|
9783
9895
|
spawnFailed = true;
|
|
9784
9896
|
clearInterval(timer);
|
|
9897
|
+
clearTimeout(limitTimer);
|
|
9785
9898
|
reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err3.message}`));
|
|
9786
9899
|
});
|
|
9787
9900
|
child.on("close", async (code) => {
|
|
9788
9901
|
clearInterval(timer);
|
|
9902
|
+
clearTimeout(limitTimer);
|
|
9789
9903
|
obs.feed(tail3.poll());
|
|
9790
9904
|
obs.end();
|
|
9791
9905
|
await endSink(eventsSink);
|
|
9792
9906
|
if (spawnFailed) return;
|
|
9793
|
-
|
|
9794
|
-
|
|
9907
|
+
if (limitTimer && !killed && !obs.sawResult && msUntilDeadline(deadline) <= 0) {
|
|
9908
|
+
killed = { limitMinutes: plan.maxRunMinutes ?? 0, elapsedMs: Date.now() - Date.parse(spawnedAt) };
|
|
9909
|
+
}
|
|
9910
|
+
const { ms: ms3, handoffPending } = await finishRun(
|
|
9911
|
+
id,
|
|
9912
|
+
obs,
|
|
9913
|
+
emit,
|
|
9914
|
+
startedAt,
|
|
9915
|
+
{ code: code ?? 1, ...killed ? { interrupted: WALL_CLOCK_REASON } : {} },
|
|
9916
|
+
plan.agentLog?.apiKey,
|
|
9917
|
+
hintMoved
|
|
9918
|
+
);
|
|
9919
|
+
resolve6({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {}, ...killed ? { killed } : {} });
|
|
9795
9920
|
});
|
|
9796
9921
|
child.stdin?.on("error", (err3) => {
|
|
9797
9922
|
emit.warn(`agent closed stdin before the prompt was delivered: ${err3.message}`, { step: "agent" });
|
|
@@ -9810,18 +9935,23 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
|
9810
9935
|
obs.feed(tail3.poll());
|
|
9811
9936
|
obs.muted = false;
|
|
9812
9937
|
const adopted = isAlive(sc);
|
|
9938
|
+
let killed;
|
|
9813
9939
|
if (adopted) {
|
|
9814
9940
|
o.onAdopted?.();
|
|
9815
9941
|
const pollMs = o.pollMs ?? TAIL_POLL_MS;
|
|
9816
9942
|
while (isAlive(sc)) {
|
|
9817
9943
|
obs.feed(tail3.poll());
|
|
9944
|
+
if (!killed && (msUntilDeadline(sc.deadline) ?? 1) <= 0) {
|
|
9945
|
+
killed = await killForDeadline(sc.pid, emit, sc.role, obs.liveTicket ?? sc.ticket, sc.spawnedAt, sc.maxRunMinutes);
|
|
9946
|
+
continue;
|
|
9947
|
+
}
|
|
9818
9948
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
9819
9949
|
}
|
|
9820
9950
|
}
|
|
9821
9951
|
obs.feed(tail3.poll());
|
|
9822
9952
|
obs.end();
|
|
9823
9953
|
await endSink(eventsSink);
|
|
9824
|
-
const interrupted = obs.sawResult ? void 0 : adopted ? "no-result" : "daemon-exit";
|
|
9954
|
+
const interrupted = killed ? WALL_CLOCK_REASON : obs.sawResult ? void 0 : adopted ? "no-result" : "daemon-exit";
|
|
9825
9955
|
const { ms: ms3, ticket } = await finishRun(
|
|
9826
9956
|
id,
|
|
9827
9957
|
obs,
|
|
@@ -9830,7 +9960,7 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
|
9830
9960
|
{ code: null, ...interrupted ? { interrupted } : {} },
|
|
9831
9961
|
o.agentLogKey
|
|
9832
9962
|
);
|
|
9833
|
-
return { adopted, ...interrupted ? { interrupted } : {}, ms: ms3, ticket };
|
|
9963
|
+
return { adopted, ...interrupted ? { interrupted } : {}, ...killed ? { killed } : {}, ms: ms3, ticket };
|
|
9834
9964
|
}
|
|
9835
9965
|
|
|
9836
9966
|
// src/seat-keys.ts
|
|
@@ -14331,7 +14461,8 @@ function describeRun(run2, route2, seatIds, worktreeOf = (d, n) => {
|
|
|
14331
14461
|
worktree: repoDir && num ? worktreeOf(repoDir, num) : null,
|
|
14332
14462
|
stream: run2.streamPath,
|
|
14333
14463
|
pid: run2.pid,
|
|
14334
|
-
startedAt: run2.spawnedAt
|
|
14464
|
+
startedAt: run2.spawnedAt,
|
|
14465
|
+
deadline: run2.deadline ?? null
|
|
14335
14466
|
};
|
|
14336
14467
|
}
|
|
14337
14468
|
|
|
@@ -15505,7 +15636,10 @@ async function discover(o) {
|
|
|
15505
15636
|
...hostPeriscopes !== void 0 ? { host_periscopes: hostPeriscopes } : {},
|
|
15506
15637
|
...sshPublicKey !== void 0 ? { ssh_public_key: sshPublicKey } : {},
|
|
15507
15638
|
...ownerId !== void 0 ? { owner_id: ownerId } : {},
|
|
15508
|
-
...writeMachineId ? { machine_id: machineId } : {}
|
|
15639
|
+
...writeMachineId ? { machine_id: machineId } : {},
|
|
15640
|
+
// ISSUE-2791: a ship connecting by hand is a developer machine; the
|
|
15641
|
+
// column arrives with a template update, so only written when present.
|
|
15642
|
+
...shipColumns.has("kind") ? { kind: "developer_machine" } : {}
|
|
15509
15643
|
});
|
|
15510
15644
|
shipRowId = created.id;
|
|
15511
15645
|
out.shipRow = { id: shipRowId, name: o.ship.name, action: "created", matchedBy };
|
|
@@ -18782,7 +18916,7 @@ async function reconcileForRoute(ctx, o) {
|
|
|
18782
18916
|
if (wt) dirty = status(wt.path).length > 0;
|
|
18783
18917
|
} catch {
|
|
18784
18918
|
}
|
|
18785
|
-
await tracker.postEvent(sc.ticketRecordId, interruptedComment({
|
|
18919
|
+
await tracker.postEvent(sc.ticketRecordId, result.killed ? runKilledComment({ limitMinutes: result.killed.limitMinutes, branch: wt?.branch ?? null, dirty }) : interruptedComment({
|
|
18786
18920
|
branch: wt?.branch ?? null,
|
|
18787
18921
|
dirty,
|
|
18788
18922
|
reason: result.interrupted === "daemon-exit" ? "the crew daemon exited" : "it exited without a result"
|
|
@@ -18805,6 +18939,23 @@ async function reconcileForRoute(ctx, o) {
|
|
|
18805
18939
|
}
|
|
18806
18940
|
});
|
|
18807
18941
|
}
|
|
18942
|
+
async function postRunKilled(route2, tracker, ticketId, memberId, limitMinutes, emit) {
|
|
18943
|
+
if (!ticketId || !memberId) return;
|
|
18944
|
+
try {
|
|
18945
|
+
const t = await tracker.ticket(ticketId);
|
|
18946
|
+
const target = repoTargetFor(route2, t.repo_id);
|
|
18947
|
+
const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
|
|
18948
|
+
const wt = target && num ? worktreeForNumber(target.dir, num) : null;
|
|
18949
|
+
let dirty = null;
|
|
18950
|
+
try {
|
|
18951
|
+
if (wt) dirty = status(wt.path).length > 0;
|
|
18952
|
+
} catch {
|
|
18953
|
+
}
|
|
18954
|
+
await tracker.postEvent(ticketId, runKilledComment({ limitMinutes, branch: wt?.branch ?? null, dirty }), memberId, "interrupted");
|
|
18955
|
+
} catch (e) {
|
|
18956
|
+
emit.warn(`could not post the wall-clock limit comment: ${e.message}`, { step: "agent" });
|
|
18957
|
+
}
|
|
18958
|
+
}
|
|
18808
18959
|
async function runRoleAgent(role, decision, ctx, hooks) {
|
|
18809
18960
|
const held = {};
|
|
18810
18961
|
try {
|
|
@@ -18967,8 +19118,12 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
18967
19118
|
} catch {
|
|
18968
19119
|
}
|
|
18969
19120
|
}
|
|
18970
|
-
await spawnAgent(plan, emit, hintMoved);
|
|
18971
|
-
|
|
19121
|
+
const result = await spawnAgent(plan, emit, hintMoved);
|
|
19122
|
+
if (result.killed) {
|
|
19123
|
+
await postRunKilled(route2, tracker, workingId, memberId, result.killed.limitMinutes, emit);
|
|
19124
|
+
} else {
|
|
19125
|
+
await guardRunHandoff(route2, ship, tracker, role, workingId, emit, runStart);
|
|
19126
|
+
}
|
|
18972
19127
|
} finally {
|
|
18973
19128
|
await settleRunEnd({
|
|
18974
19129
|
tracker,
|
|
@@ -24403,7 +24558,7 @@ watermark: ${state.watermark()}
|
|
|
24403
24558
|
);
|
|
24404
24559
|
}
|
|
24405
24560
|
for (const x of runs) {
|
|
24406
|
-
process.stdout.write(`running: ${x.role} \u2014 ${x.ticket ?? "(no ticket)"}${x.repo ? ` in ${x.repo}` : ""} (pid ${x.pid}, since ${x.spawnedAt})
|
|
24561
|
+
process.stdout.write(`running: ${x.role} \u2014 ${x.ticket ?? "(no ticket)"}${x.repo ? ` in ${x.repo}` : ""} (pid ${x.pid}, since ${x.spawnedAt}${x.deadline ? `, deadline ${x.deadline}` : ""})
|
|
24407
24562
|
`);
|
|
24408
24563
|
}
|
|
24409
24564
|
if (shipRows.length) {
|
|
@@ -24613,6 +24768,11 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
24613
24768
|
const missingAuthorship = await tracker.missingAuthorshipColumns();
|
|
24614
24769
|
process.stdout.write(
|
|
24615
24770
|
missingAuthorship.length === 0 ? "authorship: filed_by_id, ship_id present\n" : `authorship: missing ${missingAuthorship.join(", ")} \u2014 update the Issues template
|
|
24771
|
+
`
|
|
24772
|
+
);
|
|
24773
|
+
const missingRuntime = await tracker.missingRuntimeColumns();
|
|
24774
|
+
process.stdout.write(
|
|
24775
|
+
missingRuntime.length === 0 ? "runtime columns: forge_host, kind, instance_class, relay_url present\n" : `runtime columns: missing ${missingRuntime.join(", ")} \u2014 read as their defaults; update the Issues template
|
|
24616
24776
|
`
|
|
24617
24777
|
);
|
|
24618
24778
|
process.stdout.write(
|
package/package.json
CHANGED