trantor 0.17.39 → 0.17.40

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.17.39",
3
+ "version": "0.17.40",
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/baton.mjs CHANGED
@@ -30,7 +30,7 @@ function findTranscript() {
30
30
  }
31
31
 
32
32
  const transcript = findTranscript();
33
- const { file } = writeHandoff({ projectDir: cwd, sessionId: "", transcript, trigger: "manual-cli" });
33
+ const { file } = writeHandoff({ projectDir: cwd, sessionId: "", transcript, trigger: "manual-cli", force: true }); // manual = intentional, bypass the storm guard
34
34
  console.log(`📋 handoff saved for ${project}: ${file}`);
35
35
  const { spawned, armed, windowId } = spawnBaton({ projectDir: cwd, handoffFile: file });
36
36
  console.log(spawned
@@ -12,7 +12,9 @@ import { basename } from "node:path";
12
12
  const [, , projectDir = process.cwd(), sessionId = "", transcript = "", trigger = "context-warn", windowId = "", tty = ""] = process.argv;
13
13
  try {
14
14
  const conf = readConfig();
15
- const { file, record } = writeHandoff({ projectDir, sessionId, transcript, trigger });
15
+ const result = writeHandoff({ projectDir, sessionId, transcript, trigger }); // auto path — honors the hub storm guard
16
+ if (result.skipped) { process.stderr.write(`[trantor] handoff SKIPPED by storm-guard (${result.reason}; ${result.sinceSec ?? "?"}s since last) — no fresh window spawned\n`); process.exit(0); }
17
+ const { file, record } = result;
16
18
  process.stderr.write(`[trantor] baton handoff written: ${file}\n`);
17
19
  await pingBus(basename(projectDir), record.id, conf);
18
20
  if (maybeSpawn(projectDir, conf)) { // open the fresh session that takes over
@@ -20,6 +20,7 @@ import { spawn } from "node:child_process";
20
20
  import { fileURLToPath } from "node:url";
21
21
  import { readConfig, contextUsage, warnFrac, alreadyHandedOff, markHandedOff, controllingTty, terminalWindowForTty, subagentsActive } from "./lib/handoff.mjs";
22
22
  import { resolveProject, hostId } from "../lib/project.mjs";
23
+ import { installedVersion } from "./lib/update-check.mjs"; // report our hook version so the hub can flag stale sessions
23
24
 
24
25
  const HEARTBEAT_MS = Number(process.env.RELAY_HEARTBEAT_MS || 60 * 1000);
25
26
  const FETCH_TIMEOUT_MS = Number(process.env.RELAY_HEARTBEAT_TIMEOUT_MS || 1500);
@@ -121,7 +122,7 @@ async function main(stdinRaw) {
121
122
  await fetch(`${relayUrl()}/register`, {
122
123
  method: "POST",
123
124
  headers: { "content-type": "application/json" },
124
- body: JSON.stringify({ session, project }),
125
+ body: JSON.stringify({ session, project, hookVersion: (() => { try { return installedVersion(); } catch { return ""; } })() }),
125
126
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
126
127
  });
127
128
  } catch {}
@@ -213,8 +213,21 @@ export function verbatimRecentTail(transcript, chars = 7000) {
213
213
  }
214
214
 
215
215
  // ---- write + announce + spawn ----------------------------------------------
216
- export function writeHandoff({ projectDir, sessionId, transcript, trigger, summary }) {
216
+ export function writeHandoff({ projectDir, sessionId, transcript, trigger, summary, force = false }) {
217
217
  const projectName = basename(projectDir);
218
+ // Server-side storm guard: a session running OLD hooks (before the local markHandedOff guard) re-fires
219
+ // context-warn handoffs every few minutes — the crebral-cortex storm (9 in 49 min, each spawning a
220
+ // window). Ask the hub for clearance (rate-limit per project+session); a non-forced handoff inside the
221
+ // cooldown is SKIPPED — no file, no spawn. Manual (/trantor:handoff) + at-wall (precompact) handoffs
222
+ // force through. Fail-OPEN if the hub is unreachable, so a legit handoff is never blocked.
223
+ if (!force) {
224
+ try {
225
+ const body = JSON.stringify({ project: projectName, session: sessionId || "", trigger: trigger || "auto" });
226
+ const out = execSync(`curl -s --max-time 2 -X POST -H 'content-type: application/json' -d ${JSON.stringify(body)} ${JSON.stringify(relayUrl() + "/handoff")}`, { encoding: "utf8", timeout: 2500 });
227
+ const r = JSON.parse(out);
228
+ if (r && r.allow === false) return { skipped: true, reason: r.reason || "storm-guard", sinceSec: r.sinceSec };
229
+ } catch {}
230
+ }
218
231
  if (!existsSync(HANDOFF_DIR)) mkdirSync(HANDOFF_DIR, { recursive: true });
219
232
  const stamp = nowSec() || Date.now();
220
233
  let gitStatus = "";
@@ -25,7 +25,7 @@ try {
25
25
  const sessionId = input.session_id || "";
26
26
  const conf = readConfig();
27
27
 
28
- const { file, record } = writeHandoff({ projectDir, sessionId, transcript, trigger });
28
+ const { file, record } = writeHandoff({ projectDir, sessionId, transcript, trigger, force: true }); // at-wall backstop — must never be storm-guard-suppressed
29
29
  process.stderr.write(`[trantor] handoff written: ${file} (trigger=${trigger})\n`);
30
30
 
31
31
  await pingBus(projectName, record.id, conf);
package/hub.mjs CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.17.39",
3
+ "version": "0.17.40",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"
@@ -10,7 +10,7 @@
10
10
  "zod": "^4.4.3"
11
11
  },
12
12
  "scripts": {
13
- "test": "node test.mjs && node test-scenarios.mjs && node test-failure.mjs && node test-handoff.mjs && node test-agents.mjs && node test-update.mjs && node test-balances.mjs && node test-subagent-cost.mjs"
13
+ "test": "node test.mjs && node test-scenarios.mjs && node test-failure.mjs && node test-handoff.mjs && node test-agents.mjs && node test-update.mjs && node test-handoff-guard.mjs && node test-balances.mjs && node test-subagent-cost.mjs"
14
14
  },
15
15
  "description": "The hub-world for AI agent crews — orchestrate Claude Code, Codex, Gemini, Kimi & DeepSeek as live crews with a plan-aware Advisor, a Kanban/flow command center, a testing gate, and an economics brain (Scrooge).",
16
16
  "files": [
package/ui.html CHANGED
@@ -53,6 +53,7 @@ main:not(.learn-open) .learn-body{display:none}
53
53
  .agents{display:flex;gap:6px;flex-wrap:wrap}
54
54
  .agent{display:flex;align-items:center;gap:5px;background:var(--card);border:1px solid var(--line);border-radius:16px;padding:2px 9px 2px 6px;font-size:11.5px;color:var(--mut)}
55
55
  .agent .nm{color:var(--tx)}
56
+ .agent .stale{font-size:9.5px;font-weight:700;color:#ffb454;background:#2a1f10;border:1px solid #5a3c1a;border-radius:7px;padding:1px 5px;cursor:help}
56
57
  .agent svg{flex:none}
57
58
  .agent.offl{opacity:.42}
58
59
  .agent.err{border-color:#ef4444;color:#ef4444}
@@ -768,7 +769,7 @@ async function render(){
768
769
  const pmsgs = msgs.filter(m=>projOf(m)===p.project);
769
770
  const done=pt.filter(t=>t.status==='done').length;
770
771
  const pct=pt.length?Math.round(done/pt.length*100):0;
771
- const agents=p.agents.sort((a,b)=>b.online-a.online).map(a=>`<span class="agent ${a.online?'':'offl'}${a.health==='down'?' down':a.health==='errored'?' err':''}" title="${esc(a.session)}${a.online?' · online':' · offline'}${a.health&&a.health!=='ok'?' · '+a.health:''}">${iconFor(a.session,15)}<span class="nm">${esc(a.session)}</span>${a.status?` <span class="ast">· ${esc(a.status)}</span>`:''}${poolOf(a.session)?` <span class="ast" style="opacity:.7">[${esc(poolOf(a.session))}]</span>`:''}</span>`).join('');
772
+ const agents=p.agents.sort((a,b)=>b.online-a.online).map(a=>`<span class="agent ${a.online?'':'offl'}${a.health==='down'?' down':a.health==='errored'?' err':''}" title="${esc(a.session)}${a.online?' · online':' · offline'}${a.health&&a.health!=='ok'?' · '+a.health:''}${a.staleHooks?' · OLD Trantor hooks ('+esc(a.hookVersion)+') — restart this session to get the baton safety fixes':''}">${iconFor(a.session,15)}<span class="nm">${esc(a.session)}</span>${a.staleHooks?` <span class="stale" title="running old Trantor hooks (${esc(a.hookVersion)}) — restart to load the baton storm/kill safety fixes">⚠ old hooks</span>`:''}${a.status?` <span class="ast">· ${esc(a.status)}</span>`:''}${poolOf(a.session)?` <span class="ast" style="opacity:.7">[${esc(poolOf(a.session))}]</span>`:''}</span>`).join('');
772
773
  const cols=COLS.map(([k,label])=>{
773
774
  let cards=pt.filter(t=>k==='testing'?(t.status==='testing'||t.status==='failed'):t.status===k);
774
775
  if(k==='done')cards=[...cards].sort((a,b)=>(b.updated||0)-(a.updated||0)); // newest finished on top