trantor 0.17.40 → 0.17.41
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.
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + context-handoff for independent AI coding agents (Claude, Codex, Gemini, …)",
|
|
9
|
-
"version": "0.17.
|
|
9
|
+
"version": "0.17.41"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "trantor",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "The hub-world for AI agent crews. Say \"fire up the crew\" and Claude becomes the architect: a plan-aware Advisor routes the work (solo / cheap inline calls / live crew of Codex, Gemini, Kimi & DeepSeek in their own terminal windows), a Kanban/flow command center with a testing gate tracks it, and an economics brain (Scrooge) keeps the receipts. Includes the relay MCP, a SessionStart auto-discovery hook, and a PreCompact context-handoff so a fresh session can take over a full window instead of compacting.",
|
|
16
|
-
"version": "0.17.
|
|
16
|
+
"version": "0.17.41",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Sasha Bogojevic"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.41",
|
|
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/hooks/hooks.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "trantor — auto-register each session + inject live roster (SessionStart); heartbeat presence on every tool call + mirror the session's TodoWrite list onto the board as cards (PostToolUse); write a handoff before compaction (PreCompact); card each sub-agent's notional API cost when it finishes (SubagentStop)",
|
|
2
|
+
"description": "trantor — auto-register each session + inject live roster (SessionStart); heartbeat presence on every tool call + deliver unread bus messages to a busy session mid-turn + mirror the session's TodoWrite list onto the board as cards (PostToolUse); write a handoff before compaction (PreCompact); card each sub-agent's notional API cost when it finishes (SubagentStop)",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SessionStart": [
|
|
5
5
|
{ "matcher": "", "hooks": [ { "type": "command", "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/sessionstart.mjs" } ] }
|
|
6
6
|
],
|
|
7
7
|
"PostToolUse": [
|
|
8
8
|
{ "matcher": "", "hooks": [ { "type": "command", "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/heartbeat.mjs" } ] },
|
|
9
|
+
{ "matcher": "", "hooks": [ { "type": "command", "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/inbox-deliver.mjs" } ] },
|
|
9
10
|
{ "matcher": "TodoWrite", "hooks": [ { "type": "command", "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/todo-sync.mjs" } ] }
|
|
10
11
|
],
|
|
11
12
|
"PreCompact": [
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// trantor PostToolUse inbox delivery — surface bus messages to a BUSY session.
|
|
3
|
+
//
|
|
4
|
+
// The bug this fixes: Trantor message delivery is pure pull-on-demand. relay_send only
|
|
5
|
+
// enqueues on the hub; the recipient learns of a message ONLY when the model itself
|
|
6
|
+
// chooses to call relay_inbox/relay_wait. A session grinding through a long tool-use loop
|
|
7
|
+
// never makes that choice, so it sits "online" (the heartbeat keeps lastSeen fresh) but
|
|
8
|
+
// DEAF — a peer can ping it twice over 10 minutes and get no reply. (Observed 2026-06-23:
|
|
9
|
+
// a new session pinged a mid-build sibling; the sibling never answered because it was busy
|
|
10
|
+
// and never polled.)
|
|
11
|
+
//
|
|
12
|
+
// The fix, hook-side (don't trust the model to poll): a busy session IS firing tool calls,
|
|
13
|
+
// so this PostToolUse hook runs constantly. Each run polls /inbox and injects any NEW peer
|
|
14
|
+
// messages via hookSpecificOutput.additionalContext — which Claude Code delivers as a
|
|
15
|
+
// system reminder the model acts on IN THE SAME TURN, between its own tool calls. So a ping
|
|
16
|
+
// lands within a few seconds even mid-build, and the model can reply via relay_send without
|
|
17
|
+
// waiting for the human to prompt it.
|
|
18
|
+
//
|
|
19
|
+
// Cheap + fail-silent by contract: a per-session poll stamp gates the network call, a short
|
|
20
|
+
// fetch timeout means we never add real latency, and we ALWAYS exit clean with valid stdout.
|
|
21
|
+
// First run initialises the cursor to "now" (current max id) and injects NOTHING, so a
|
|
22
|
+
// session is never flooded with the whole backlog of old broadcasts on its first tool call.
|
|
23
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { homedir } from "node:os";
|
|
26
|
+
import { resolveProject, hostId } from "../lib/project.mjs";
|
|
27
|
+
|
|
28
|
+
const POLL_MS = Number(process.env.RELAY_INBOX_POLL_MS || 4000);
|
|
29
|
+
const FETCH_TIMEOUT_MS = Number(process.env.RELAY_INBOX_TIMEOUT_MS || 1500);
|
|
30
|
+
|
|
31
|
+
function relayUrl() {
|
|
32
|
+
if (process.env.RELAY_URL) return process.env.RELAY_URL;
|
|
33
|
+
try {
|
|
34
|
+
const cfg = join(homedir(), ".agent-bus", "config.json");
|
|
35
|
+
if (existsSync(cfg)) { const u = JSON.parse(readFileSync(cfg, "utf8")).url; if (u) return u; }
|
|
36
|
+
} catch {}
|
|
37
|
+
return "http://127.0.0.1:4477";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Keep injected text safe to embed in JSON: drop control chars that could corrupt the
|
|
41
|
+
// additionalContext payload (the model still gets the readable message).
|
|
42
|
+
function sanitize(s) { return String(s == null ? "" : s).replace(/[\x00-\x1f\x7f-\x9f]/g, " "); }
|
|
43
|
+
|
|
44
|
+
async function getInbox(url, session, since) {
|
|
45
|
+
const r = await fetch(`${url}/inbox?session=${encodeURIComponent(session)}&since=${since}`,
|
|
46
|
+
{ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
47
|
+
if (!r.ok) throw new Error(`hub ${r.status}`);
|
|
48
|
+
return r.json(); // { messages: [...], cursor }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// PostToolUse hands us the tool-input JSON on stdin. We don't need it, but we must DRAIN it:
|
|
52
|
+
// a large tool input (e.g. a big Write) can exceed the 64KB pipe buffer and block the parent's
|
|
53
|
+
// write if nobody reads. Consume + discard, with a short timeout so we never hang.
|
|
54
|
+
function drainStdin() {
|
|
55
|
+
return new Promise(res => {
|
|
56
|
+
try { process.stdin.resume(); process.stdin.on("data", () => {}); process.stdin.on("end", res); }
|
|
57
|
+
catch { res(); }
|
|
58
|
+
setTimeout(res, 80);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Self-validating stdout: model-facing additionalContext only when we actually deliver.
|
|
63
|
+
function emit(ctx) {
|
|
64
|
+
if (!ctx) return "{}";
|
|
65
|
+
const obj = { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: ctx } };
|
|
66
|
+
const out = JSON.stringify(obj);
|
|
67
|
+
try { JSON.parse(out); return out; } catch { return "{}"; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function main() {
|
|
71
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
72
|
+
// Mirror heartbeat.mjs / sessionstart.mjs: a home-directory session isn't project work and
|
|
73
|
+
// isn't on the bus — nothing to deliver. Opt in with RELAY_SESSION / RELAY_PROJECT.
|
|
74
|
+
if (!process.env.RELAY_SESSION && !process.env.RELAY_PROJECT && projectDir === homedir()) return "{}";
|
|
75
|
+
|
|
76
|
+
// Resolve THIS session's identity EXACTLY as mcp.mjs / heartbeat.mjs do, so we poll the
|
|
77
|
+
// same peer the relay registered (RELAY_SESSION wins; else RELAY_AGENT brand; else host:project).
|
|
78
|
+
const project = resolveProject(projectDir);
|
|
79
|
+
const session = process.env.RELAY_SESSION
|
|
80
|
+
|| (process.env.RELAY_AGENT ? `${process.env.RELAY_AGENT}:${project}` : `${hostId()}:${project}`);
|
|
81
|
+
|
|
82
|
+
const safe = session.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
83
|
+
const dir = join(homedir(), ".agent-bus");
|
|
84
|
+
const pollStamp = join(dir, `inbox-poll-${safe}.stamp`);
|
|
85
|
+
const cursorFile = join(dir, `inbox-cursor-${safe}.id`);
|
|
86
|
+
|
|
87
|
+
// Throttle: poll the hub at most once per POLL_MS. Write the stamp BEFORE the network call
|
|
88
|
+
// so a burst of parallel tool calls doesn't all fire (and double-deliver).
|
|
89
|
+
try {
|
|
90
|
+
if (existsSync(pollStamp)) {
|
|
91
|
+
const last = Number(readFileSync(pollStamp, "utf8")) || 0;
|
|
92
|
+
if (Date.now() - last < POLL_MS) return "{}"; // within window — skip
|
|
93
|
+
}
|
|
94
|
+
} catch {}
|
|
95
|
+
try { writeFileSync(pollStamp, String(Date.now())); } catch {}
|
|
96
|
+
|
|
97
|
+
const url = relayUrl();
|
|
98
|
+
|
|
99
|
+
// First run: no cursor yet. Initialise to the current max deliverable id and inject NOTHING,
|
|
100
|
+
// so we start listening "from now" instead of replaying the whole backlog of old broadcasts.
|
|
101
|
+
if (!existsSync(cursorFile)) {
|
|
102
|
+
try {
|
|
103
|
+
const { cursor } = await getInbox(url, session, 0);
|
|
104
|
+
writeFileSync(cursorFile, String(cursor || 0));
|
|
105
|
+
} catch {}
|
|
106
|
+
return "{}";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let cursor = 0;
|
|
110
|
+
try { cursor = Number(readFileSync(cursorFile, "utf8")) || 0; } catch {}
|
|
111
|
+
|
|
112
|
+
let messages = [], next = cursor;
|
|
113
|
+
try {
|
|
114
|
+
const res = await getInbox(url, session, cursor);
|
|
115
|
+
messages = Array.isArray(res.messages) ? res.messages : [];
|
|
116
|
+
next = res.cursor || cursor;
|
|
117
|
+
} catch { return "{}"; } // hub down / timeout — never block the tool flow
|
|
118
|
+
|
|
119
|
+
if (!messages.length) return "{}";
|
|
120
|
+
|
|
121
|
+
// Advance the cursor immediately so we don't re-inject these on the next tool call.
|
|
122
|
+
try { writeFileSync(cursorFile, String(next)); } catch {}
|
|
123
|
+
|
|
124
|
+
const lines = messages.map(m => {
|
|
125
|
+
const direct = m.to === session;
|
|
126
|
+
const tag = direct ? "📨 DIRECT" : "📣 broadcast";
|
|
127
|
+
const when = (() => { try { return new Date(m.ts).toLocaleTimeString(); } catch { return ""; } })();
|
|
128
|
+
return `- ${tag} from ${sanitize(m.from)}${when ? ` (${when})` : ""}: ${sanitize(m.text)}`;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const ctx =
|
|
132
|
+
`<trantor-inbox count="${messages.length}">\n` +
|
|
133
|
+
`📬 ${messages.length} new bus message(s) arrived while you were working (you did not poll for these — Trantor surfaced them automatically):\n` +
|
|
134
|
+
lines.join("\n") + `\n` +
|
|
135
|
+
`If a peer is asking you something or waiting on you, reply now with the relay_send tool (to their session id). ` +
|
|
136
|
+
`If a message just needs an ack, send a short one. You can keep working after responding.\n` +
|
|
137
|
+
`</trantor-inbox>\n`;
|
|
138
|
+
|
|
139
|
+
return emit(ctx);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Never block or break the tool flow: drain stdin, swallow everything, always emit valid stdout.
|
|
143
|
+
drainStdin()
|
|
144
|
+
.then(main)
|
|
145
|
+
.then(out => { try { process.stdout.write(out || "{}"); } catch {} })
|
|
146
|
+
.catch(() => { try { process.stdout.write("{}"); } catch {} })
|
|
147
|
+
.finally(() => process.exit(0));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.41",
|
|
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-handoff-guard.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 && node test-inbox.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": [
|