trantor 0.17.65 → 0.17.66
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/hub.mjs +34 -10
- package/lib/overseer.mjs +34 -5
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.66",
|
|
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/hub.mjs
CHANGED
|
@@ -212,8 +212,16 @@ async function reloadFromStore() {
|
|
|
212
212
|
let _overseer = null;
|
|
213
213
|
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
|
-
|
|
216
|
-
|
|
215
|
+
// How long a condition must be ABSENT before we consider the episode over. This is NOT a re-warn
|
|
216
|
+
// timer: see overseerTick.
|
|
217
|
+
const OVERSEER_CLEAR_MS = Number(process.env.RELAY_OVERSEER_CLEAR_MS || process.env.RELAY_OVERSEER_DEDUP_MS || 10 * 60 * 1000);
|
|
218
|
+
// Standing conditions, keyed by collision identity -> { since, lastTick }. A collision is a STATE,
|
|
219
|
+
// not an event: it persists. Emitting on a 10-minute timer turned the watcher into a metronome —
|
|
220
|
+
// 500 events for 4 distinct conditions (2026-08-12 audit), each one also waking the duty seat for a
|
|
221
|
+
// full turn. Now an episode fires ONCE when it starts and stays quiet while it holds; the entry is
|
|
222
|
+
// forgotten only after the condition has been gone for OVERSEER_CLEAR_MS, so a genuine recurrence
|
|
223
|
+
// warns again.
|
|
224
|
+
const overseerActive = new Map();
|
|
217
225
|
// Heartbeat for the WATCHER itself: /overseer/status must distinguish "fleet is clear" from "the
|
|
218
226
|
// overseer stopped ticking" — a monitor that cannot prove it is alive reads as clear when dead.
|
|
219
227
|
let overseerLastTick = 0;
|
|
@@ -240,14 +248,18 @@ function overseerTick() {
|
|
|
240
248
|
if (!_overseer?.detectCollisions) return;
|
|
241
249
|
let collisions = [];
|
|
242
250
|
try { collisions = _overseer.detectCollisions(overseerInputs()) || []; } catch { return; }
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
for (const [k, ts] of overseerWarned) if (ts < cut) overseerWarned.delete(k);
|
|
251
|
+
const t = now();
|
|
252
|
+
overseerLastTick = t;
|
|
246
253
|
const pol = overseerPolicy();
|
|
254
|
+
const seen = new Set();
|
|
247
255
|
for (const c of collisions) {
|
|
248
256
|
const key = `${c.project} ${c.kind} ${(c.sessions || []).join(",")} ${(c.files || []).join(",")}`;
|
|
249
|
-
|
|
250
|
-
|
|
257
|
+
c.key = key;
|
|
258
|
+
seen.add(key);
|
|
259
|
+
const standing = overseerActive.get(key);
|
|
260
|
+
if (standing) { standing.lastTick = t; c.since = standing.since; continue; } // holds — stay quiet
|
|
261
|
+
overseerActive.set(key, { since: t, lastTick: t });
|
|
262
|
+
c.since = t;
|
|
251
263
|
appendEvent("overseer.warn", c.project, "overseer",
|
|
252
264
|
{ kind: c.kind, sessions: c.sessions || [], files: c.files || [], detail: c.detail || "", narrated: false });
|
|
253
265
|
if (DUTY_SESSION) hubSend(DUTY_SESSION, `⚠️ OVERSEER ${c.kind} [${c.project}]: ${c.detail || ""} — if the parties are not already coordinating, message them.`, c.project);
|
|
@@ -260,8 +272,18 @@ function overseerTick() {
|
|
|
260
272
|
appendEvent("verify.gate.opened", c.project, "overseer", { gateId: g.id, claim: g.claim, why: g.why });
|
|
261
273
|
}
|
|
262
274
|
}
|
|
275
|
+
// Episode end: a condition gone for the whole clear window is over, so a LATER recurrence is a
|
|
276
|
+
// new episode and warns again. Without this the map would grow forever and nothing could re-fire.
|
|
277
|
+
for (const [k, v] of overseerActive) {
|
|
278
|
+
if (!seen.has(k) && t - v.lastTick > OVERSEER_CLEAR_MS) overseerActive.delete(k);
|
|
279
|
+
}
|
|
280
|
+
overseerLastCollisions = collisions;
|
|
263
281
|
}
|
|
264
282
|
setInterval(overseerTick, OVERSEER_TICK_MS).unref?.();
|
|
283
|
+
// setInterval waits a FULL period before its first call, so for 30s after every restart the watcher
|
|
284
|
+
// had no lastTick and honestly reported itself stalled. Tick once shortly after boot (the delay lets
|
|
285
|
+
// the lazy lib import land) so a restarted hub proves it is alive immediately.
|
|
286
|
+
setTimeout(overseerTick, 2000).unref?.();
|
|
265
287
|
|
|
266
288
|
// --- the DUTY AGENT feed (deterministic escalation; the seat itself is bin/duty.mjs) -----------
|
|
267
289
|
// RELAY_DUTY_SESSION names the always-on triage seat (e.g. "claude:fleet"). The hub DMs it when:
|
|
@@ -1030,7 +1052,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1030
1052
|
engine: !!_overseer?.detectCollisions,
|
|
1031
1053
|
lastTickTs: overseerLastTick,
|
|
1032
1054
|
tickMs: OVERSEER_TICK_MS,
|
|
1033
|
-
|
|
1055
|
+
clearMs: OVERSEER_CLEAR_MS,
|
|
1034
1056
|
dutySession: DUTY_SESSION || "",
|
|
1035
1057
|
watching: {
|
|
1036
1058
|
sessions: livePeers.length,
|
|
@@ -1040,8 +1062,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
1040
1062
|
},
|
|
1041
1063
|
autonomy: pol.autonomy,
|
|
1042
1064
|
links: pol.links,
|
|
1043
|
-
|
|
1044
|
-
|
|
1065
|
+
// `since` turns a detection into a duration — "standing 4h" reads very differently from
|
|
1066
|
+
// "just started", and that distinction is the whole point of episode-based warning.
|
|
1067
|
+
warnings: overseerLastCollisions.map(c => ({ ...c, since: c.since || 0 })),
|
|
1068
|
+
standing: overseerActive.size,
|
|
1045
1069
|
});
|
|
1046
1070
|
}
|
|
1047
1071
|
if (req.method === "GET" && P === "/overseer/context") {
|
package/lib/overseer.mjs
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Windows are read from env ONCE at load (calls stay pure): without an override, a test cannot
|
|
2
|
+
// observe a condition CLEARING — peers stay "live" for five minutes no matter what the test does,
|
|
3
|
+
// so the episode-recurrence path was untestable and therefore unproven.
|
|
4
|
+
const PEER_LIVE_MS = Number(process.env.RELAY_OVERSEER_PEER_LIVE_MS || 5 * 60 * 1000);
|
|
5
|
+
const CLAIM_LIVE_MS = Number(process.env.RELAY_OVERSEER_CLAIM_LIVE_MS || 10 * 60 * 1000);
|
|
6
|
+
// "Actively executing", not merely "window open" — the same 90s bar the desktop app calls `busy`
|
|
7
|
+
// (the heartbeat fires on tool calls, so a fresher-than-90s peer is mid-turn). Presence alone is
|
|
8
|
+
// too weak a signal to call a collision: see the linked-activity note below.
|
|
9
|
+
const WORK_LIVE_MS = Number(process.env.RELAY_OVERSEER_WORK_LIVE_MS || 90 * 1000);
|
|
3
10
|
|
|
4
11
|
const KINDS = new Set(["same-project-sessions", "file-conflict", "linked-activity"]);
|
|
5
12
|
|
|
@@ -119,18 +126,40 @@ export function detectCollisions({ peers = [], claims = [], links = [], autonomy
|
|
|
119
126
|
});
|
|
120
127
|
}
|
|
121
128
|
|
|
129
|
+
// A link is DECLARED — the operator already told us these projects move together. Warning merely
|
|
130
|
+
// because both have a session OPEN restates that declaration, and it stays true for hours:
|
|
131
|
+
// crebral-health ↔ crebral-scribe produced 468 identical warnings across 8 days (2026-08-12
|
|
132
|
+
// audit), which is how a monitor teaches you to ignore it. The signal worth raising is
|
|
133
|
+
// CONCURRENT WORK — each side actually executing (heartbeat inside WORK_LIVE_MS) or holding a
|
|
134
|
+
// fresh file claim. Two idle-open windows are not a collision.
|
|
135
|
+
const workingByProject = new Map();
|
|
136
|
+
for (const peer of livePeers) {
|
|
137
|
+
if (!isFresh(peer?.lastSeen, at, WORK_LIVE_MS)) continue;
|
|
138
|
+
const sessions = workingByProject.get(peer.project) ?? [];
|
|
139
|
+
sessions.push(peer.session);
|
|
140
|
+
workingByProject.set(peer.project, sessions);
|
|
141
|
+
}
|
|
142
|
+
for (const claim of asArray(claims)) {
|
|
143
|
+
const project = clean(claim?.project);
|
|
144
|
+
const session = clean(claim?.session);
|
|
145
|
+
if (!project || !session || !isFresh(claim?.ts, at, CLAIM_LIVE_MS)) continue;
|
|
146
|
+
const sessions = workingByProject.get(project) ?? [];
|
|
147
|
+
sessions.push(session);
|
|
148
|
+
workingByProject.set(project, sessions);
|
|
149
|
+
}
|
|
150
|
+
|
|
122
151
|
for (const link of asArray(links)) {
|
|
123
152
|
const projects = sortedStrings(link?.projects ?? []);
|
|
124
153
|
if (projects.length < 2) continue;
|
|
125
|
-
const activeProjects = projects.filter((project) => (
|
|
154
|
+
const activeProjects = projects.filter((project) => (workingByProject.get(project) ?? []).length > 0);
|
|
126
155
|
if (activeProjects.length < 2) continue;
|
|
127
|
-
const sessions = sortedStrings(activeProjects.flatMap((project) =>
|
|
156
|
+
const sessions = sortedStrings(activeProjects.flatMap((project) => workingByProject.get(project) ?? []));
|
|
128
157
|
pushCollision(out, {
|
|
129
158
|
project: activeProjects[0],
|
|
130
159
|
kind: "linked-activity",
|
|
131
160
|
sessions,
|
|
132
161
|
files: [],
|
|
133
|
-
detail: `Linked projects ${activeProjects.join(", ")}
|
|
162
|
+
detail: `Linked projects ${activeProjects.join(", ")} are being worked on at the same time by ${sessions.join(", ")}.`,
|
|
134
163
|
});
|
|
135
164
|
}
|
|
136
165
|
|