trantor 0.18.24 → 0.18.26
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/.claude-plugin/plugin.json +1 -1
- package/bin/crew-runner.mjs +41 -9
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.26",
|
|
4
4
|
"description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + crew orchestration for independent AI coding agents (Claude, Codex, Gemini, Kimi, DeepSeek)",
|
|
5
5
|
"mcpServers": {
|
|
6
6
|
"relay": {
|
package/bin/crew-runner.mjs
CHANGED
|
@@ -266,6 +266,10 @@ let consecFails = 0;
|
|
|
266
266
|
// every live seat, so two working agents spent the evening reading the same sentence.
|
|
267
267
|
let announced = "";
|
|
268
268
|
let lastErrText = "";
|
|
269
|
+
// #5481: the turn exited 0 with a NULL/empty transcript — the Inception/Mercury trap. The provider
|
|
270
|
+
// burned its whole max_tokens budget on internal reasoning and returned a null completion; the
|
|
271
|
+
// runner used to read that silence as a clean turn while nothing was produced.
|
|
272
|
+
let lastEmptyOutput = false;
|
|
269
273
|
const ERRF = join(homedir(), ".agent-bus", `err-${AGENT}-${PROJ}.txt`);
|
|
270
274
|
|
|
271
275
|
// ---- undelivered wake messages (the runner owns delivery, not the hub) ----
|
|
@@ -312,7 +316,9 @@ function loadPending() {
|
|
|
312
316
|
// classifyFailure's set (no bare /expired/) so a healthy transcript never trips it.
|
|
313
317
|
const AUTH_MARKER_RE = /unauthor|401|403|forbidden|invalid[ _-]?api[ _-]?key|authentication? failed|token expired/i;
|
|
314
318
|
|
|
315
|
-
function classifyFailure(exit, errText) {
|
|
319
|
+
function classifyFailure(exit, errText, emptyOutput = false) {
|
|
320
|
+
// #5481: silence with a clean exit is a failure shape, not success — see lastEmptyOutput.
|
|
321
|
+
if (emptyOutput) return "empty-output";
|
|
316
322
|
const t = (errText || "").toLowerCase();
|
|
317
323
|
if (exit === 127) return "missing-cli";
|
|
318
324
|
// #5684: a provider BACKEND failure is not quota — it wants retry/swap, not a window wait.
|
|
@@ -329,14 +335,20 @@ function classifyFailure(exit, errText) {
|
|
|
329
335
|
|
|
330
336
|
async function reportFailure(exit, trigger, undelivered = 0) {
|
|
331
337
|
consecFails++;
|
|
332
|
-
const reason = classifyFailure(exit, lastErrText);
|
|
338
|
+
const reason = classifyFailure(exit, lastErrText, lastEmptyOutput);
|
|
333
339
|
const down = consecFails >= 2;
|
|
334
340
|
const status = down ? `down: ${reason} · ${consecFails} fails` : `errored: ${reason}`;
|
|
335
341
|
await api("/register", { session: SESSION, project: PROJ, status, llm: AGENT, model: MODEL }).catch(() => {});
|
|
336
342
|
const hint = reason === "exhausted" ? " — needs `trantor swap`"
|
|
337
343
|
: reason === "auth" ? " — check credentials"
|
|
338
344
|
: reason === "backend-error" ? " — provider backend error (NOT quota): retry, or `trantor swap` to another provider"
|
|
339
|
-
: reason === "missing-cli" ? " — CLI not on PATH"
|
|
345
|
+
: reason === "missing-cli" ? " — CLI not on PATH"
|
|
346
|
+
// #5481: name the suspected trap, not just the symptom — the dial lives in the provider's
|
|
347
|
+
// opencode model config (limit.output), not in the runner.
|
|
348
|
+
: reason === "empty-output" ? (AGENT === "inception"
|
|
349
|
+
? " — inception: raise max_tokens — diffusion burns budget on reasoning"
|
|
350
|
+
: " — exit 0 with NULL output: raise the provider's max_tokens (reasoning may be eating the budget)")
|
|
351
|
+
: "";
|
|
340
352
|
// The count of messages this seat is HOLDING is the operator-actionable half of a failure: a
|
|
341
353
|
// crashed pulse costs nothing, a crashed turn sitting on three escalations is someone waiting.
|
|
342
354
|
const held = undelivered ? ` · holding ${undelivered} undelivered message${undelivered > 1 ? "s" : ""} (will retry)` : "";
|
|
@@ -424,7 +436,8 @@ function runTurn(prompt, isFirst, trigger = "kickoff") {
|
|
|
424
436
|
cmuxStatus("building", "#4a90d9", "hammer", { priority: 50 }); herdrAgent("working");
|
|
425
437
|
// inherit stdio so the window shows the agent working live; also capture for sid-parsing.
|
|
426
438
|
// Tee stderr to ERRF (still shown live in the window) so a failed turn can be classified.
|
|
427
|
-
try { appendFileSync(ERRF, "", { flag: "w" }); } catch {}
|
|
439
|
+
try { appendFileSync(ERRF, "", { flag: "w" }); } catch {} // truncate
|
|
440
|
+
lastEmptyOutput = false;
|
|
428
441
|
// pipefail: without it the sid-capture `| tee` makes a FAILED turn exit 0 (tee's status),
|
|
429
442
|
// so the failure reporter never fires and a dead seat heartbeats green on the bus.
|
|
430
443
|
// A CLI's own explanation for quitting often goes to STDOUT, not stderr — Claude's usage-limit
|
|
@@ -474,8 +487,20 @@ function runTurn(prompt, isFirst, trigger = "kickoff") {
|
|
|
474
487
|
effExit = 1;
|
|
475
488
|
log("\x1b[31mexit 0 but turn output shows an auth failure — treating as FAILED (auth)\x1b[0m");
|
|
476
489
|
}
|
|
477
|
-
|
|
478
|
-
|
|
490
|
+
// #5481: the Inception/Mercury trap — exit 0 with a NULL completion. ERRF is the TOTAL output
|
|
491
|
+
// capture, not just stderr: every seat's stdout is tee'd into it (`| tee -a ERRF` for the
|
|
492
|
+
// opencode family, `| tee /dev/stderr` + the stderr tee for sid seats — line ~448). So an
|
|
493
|
+
// empty ERRF on a clean exit means the turn produced nothing on EITHER stream — and every
|
|
494
|
+
// real CLI prints something on success (drill C pins that), so silence is the trap, not a
|
|
495
|
+
// quiet victory. (Integration note: this was nearly "fixed" into stdout-only detection that
|
|
496
|
+
// never fired — the tee topology is the load-bearing fact; keep this comment with it.)
|
|
497
|
+
if (realExit === 0 && effExit === 0 && !lastErrText.trim()) {
|
|
498
|
+
effExit = 1;
|
|
499
|
+
lastEmptyOutput = true;
|
|
500
|
+
log("\x1b[31mexit 0 but the turn produced NO output — treating as FAILED (empty-output)\x1b[0m");
|
|
501
|
+
}
|
|
502
|
+
telemetry({ ts: Date.now(), agent: AGENT, project: PROJ, turn: TURN, trigger, model: MODEL || "default", duration_ms: Date.now() - t0, exit: realExit, effExit, authFailed: effExit !== realExit, emptyOutput: lastEmptyOutput });
|
|
503
|
+
log(`turn ended (exit ${realExit}${effExit !== realExit ? ` → effective ${effExit} (${lastEmptyOutput ? "empty-output" : "auth"})` : ""}, ${((Date.now() - t0) / 1000).toFixed(0)}s)`);
|
|
479
504
|
if (realExit === 0 && effExit === 0) { cmuxStatus("idle", "#8a94a6", "robot"); herdrAgent("idle"); } // finished this turn, waiting for the next
|
|
480
505
|
return effExit;
|
|
481
506
|
}
|
|
@@ -584,9 +609,16 @@ function composedTurn({ base = "", wakeText = "", ctxText = "", againText = "",
|
|
|
584
609
|
// never wake on your own broadcasts: a claude seat's report contains "claude:" and matched the
|
|
585
610
|
// @mention filter, buying one echo turn per report (seen live on the first pulsed orchestrator)
|
|
586
611
|
msgs = msgs.filter(m => m.from !== SESSION);
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
612
|
+
// #5760 (the night of 08-31): the hub's hourly "same-project-sessions" FYI woke every seat
|
|
613
|
+
// into a real CLI turn — three wedged for hours mid-chatter, one on the metered pool. That
|
|
614
|
+
// kind is pure coordination CONTEXT ("no human needs to relay this" — and no turn needs to
|
|
615
|
+
// burn on it either): batch it like a broadcast. file-conflict and linked-activity overseer
|
|
616
|
+
// warnings still wake — those are actionable by the seat right now.
|
|
617
|
+
const fyi = msgs.filter(m => m.from === "hub:duty" && String(m.text || "").startsWith("🤝 OVERSEER same-project-sessions"));
|
|
618
|
+
const rest = msgs.filter(m => !fyi.includes(m));
|
|
619
|
+
const direct = rest.filter(m => m.to === SESSION);
|
|
620
|
+
const mentions = rest.filter(m => m.to === "all" && (m.text.includes(`@${AGENT}`) || m.text.toLowerCase().includes(`${AGENT}:`)));
|
|
621
|
+
const bcast = [...rest.filter(m => m.to === "all" && !mentions.includes(m)), ...fyi];
|
|
590
622
|
pendingBcast.push(...bcast); // wake-policy: plain broadcasts batch, they don't wake
|
|
591
623
|
const wake = [...direct, ...mentions];
|
|
592
624
|
if (!wake.length) { if (bcast.length) { savePending(pendingWake, pendingBcast); log(`${bcast.length} broadcast(s) batched (no wake) — ${pendingBcast.length} pending`); } continue; }
|