trantor 0.18.24 → 0.18.25

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.24",
3
+ "version": "0.18.25",
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": {
@@ -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
- 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 });
478
- log(`turn ended (exit ${realExit}${effExit !== realExit ? ` effective ${effExit} (auth)` : ""}, ${((Date.now() - t0) / 1000).toFixed(0)}s)`);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.24",
3
+ "version": "0.18.25",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"