trantor 0.17.64 → 0.17.65

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.64",
3
+ "version": "0.17.65",
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": {
@@ -147,6 +147,22 @@ async function main(stdinRaw) {
147
147
  }
148
148
  } catch {}
149
149
 
150
+ // Overseer narration, same ambient pattern (10-min machine-wide stamp): the narrate worker was
151
+ // built "to run ambiently from the heartbeat" but was never actually wired in — warns sat
152
+ // mechanical forever unless someone ran it by hand. The worker exits in one cheap signed GET per
153
+ // hub when nothing is unnarrated; Scrooge is only invoked when there IS a warn to explain.
154
+ try {
155
+ const narStamp = join(homedir(), ".agent-bus", "narrate.stamp");
156
+ const last = existsSync(narStamp) ? Number(readFileSync(narStamp, "utf8")) || 0 : 0;
157
+ if (Date.now() - last > 10 * 60 * 1000) {
158
+ writeFileSync(narStamp, String(Date.now()));
159
+ const worker = spawn(process.execPath, [join(HERE, "..", "bin", "overseer-narrate.mjs"), "--quiet"], {
160
+ detached: true, stdio: "ignore",
161
+ });
162
+ worker.unref();
163
+ }
164
+ } catch {}
165
+
150
166
  // Same cadence as the presence ping: check context pressure and hand off early
151
167
  // if we've crossed the warn threshold of a known window.
152
168
  await maybeEarlyWarn(stdinRaw, session);
package/hub.mjs CHANGED
@@ -214,6 +214,10 @@ import("./lib/overseer.mjs").then(m => { _overseer = m; }).catch(() => {});
214
214
  const OVERSEER_TICK_MS = Number(process.env.RELAY_OVERSEER_TICK_MS || 30 * 1000);
215
215
  const OVERSEER_DEDUP_MS = Number(process.env.RELAY_OVERSEER_DEDUP_MS || 10 * 60 * 1000);
216
216
  const overseerWarned = new Map(); // dedup key -> ts
217
+ // Heartbeat for the WATCHER itself: /overseer/status must distinguish "fleet is clear" from "the
218
+ // overseer stopped ticking" — a monitor that cannot prove it is alive reads as clear when dead.
219
+ let overseerLastTick = 0;
220
+ let overseerLastCollisions = [];
217
221
  function overseerPolicy() {
218
222
  const p = state.orgPolicy && typeof state.orgPolicy === "object" ? state.orgPolicy : {};
219
223
  return {
@@ -236,6 +240,7 @@ function overseerTick() {
236
240
  if (!_overseer?.detectCollisions) return;
237
241
  let collisions = [];
238
242
  try { collisions = _overseer.detectCollisions(overseerInputs()) || []; } catch { return; }
243
+ overseerLastTick = now(); overseerLastCollisions = collisions;
239
244
  const cut = now() - OVERSEER_DEDUP_MS;
240
245
  for (const [k, ts] of overseerWarned) if (ts < cut) overseerWarned.delete(k);
241
246
  const pol = overseerPolicy();
@@ -495,7 +500,7 @@ function cmpSemver(a, b) {
495
500
  const AUTH_HEADERS = ["x-trantor-pubkey", "x-trantor-sig", "x-trantor-ts", "x-trantor-nonce"];
496
501
  const PUBLIC_ENDPOINTS = new Set(["/", "/ui", "/health", "/enroll"]);
497
502
  const OWNER_ENDPOINTS = new Set(["/project/delete", "/sweep", "/reconcile", "/invite", "/import", "/policy"]);
498
- const READ_ENDPOINTS = new Set(["/peers", "/tasks", "/events", "/inbox", "/peer", "/card", "/stream", "/history", "/projects", "/catchup", "/phases", "/recent", "/handoffs", "/verify-gates", "/claims", "/overseer/context"]);
503
+ const READ_ENDPOINTS = new Set(["/peers", "/tasks", "/events", "/inbox", "/peer", "/card", "/stream", "/history", "/projects", "/catchup", "/phases", "/recent", "/handoffs", "/verify-gates", "/claims", "/overseer/context", "/overseer/status"]);
499
504
  const roleRank = { read: 1, write: 2, owner: 3 };
500
505
  const hasAuthHeaders = (req) => AUTH_HEADERS.some(h => !!req.headers[h]);
501
506
  const authPath = (u) => `${u.pathname}${u.search || ""}`;
@@ -1013,6 +1018,32 @@ const server = http.createServer(async (req, res) => {
1013
1018
  }
1014
1019
  // What a session arriving on <project> needs to know: its autonomy level, who else is live,
1015
1020
  // which files are in flight, which projects are declared codependent, current collisions.
1021
+ if (req.method === "GET" && P === "/overseer/status") {
1022
+ // The Overseer view's backbone: is the watcher ALIVE, and what is it watching right now.
1023
+ // `warnings` is the LIVE detection result from the last tick (pre-dedup), not the event log —
1024
+ // the log answers "what did it do", this answers "what does it see".
1025
+ const pol = overseerPolicy();
1026
+ const cutoff = now() - ONLINE_MS;
1027
+ const livePeers = Object.entries(state.peers).filter(([, v]) => v.lastSeen > cutoff);
1028
+ pruneClaims();
1029
+ return json(res, 200, {
1030
+ engine: !!_overseer?.detectCollisions,
1031
+ lastTickTs: overseerLastTick,
1032
+ tickMs: OVERSEER_TICK_MS,
1033
+ dedupMs: OVERSEER_DEDUP_MS,
1034
+ dutySession: DUTY_SESSION || "",
1035
+ watching: {
1036
+ sessions: livePeers.length,
1037
+ projects: new Set(livePeers.map(([, v]) => v.project).filter(Boolean)).size,
1038
+ claims: fileClaims.size,
1039
+ links: pol.links.length,
1040
+ },
1041
+ autonomy: pol.autonomy,
1042
+ links: pol.links,
1043
+ warnings: overseerLastCollisions,
1044
+ warnedRecent: overseerWarned.size,
1045
+ });
1046
+ }
1016
1047
  if (req.method === "GET" && P === "/overseer/context") {
1017
1048
  const proj = canon(String(q.project || "").slice(0, 80));
1018
1049
  if (!proj) return json(res, 400, { error: "project required" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.17.64",
3
+ "version": "0.17.65",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"