trantor 0.17.5 → 0.17.6

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.5"
9
+ "version": "0.17.6"
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.5",
16
+ "version": "0.17.6",
17
17
  "author": {
18
18
  "name": "Sasha Bogojevic"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.17.5",
3
+ "version": "0.17.6",
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
@@ -250,7 +250,8 @@ const server = http.createServer(async (req, res) => {
250
250
  // ts must not throw (new Date(NaN).toISOString() does) and 500 the whole endpoint — return null
251
251
  // and let callers skip that day-bucket.
252
252
  const dayOf = ts => { const n = Number(ts); if (!n) return null; const d = new Date(n > 2e10 ? n : n * 1000); return Number.isNaN(d.getTime()) ? null : d.toISOString().slice(0, 10); };
253
- const out = { totals: {}, lessons: { global: [], byAgent: {}, byProject: {}, projects: [] }, agents: [], models: [] };
253
+ const ALL = "*"; // the cross-project ("All projects") bucket
254
+ const out = { totals: {}, lessons: { global: [], byAgent: {}, byProject: {}, projects: [] }, agents: [], agentsByProject: {}, models: [], modelsByProject: {} };
254
255
 
255
256
  // relay lessons → global / by-agent / by-project (project derived from the recorder's session id)
256
257
  const projSet = new Set();
@@ -259,55 +260,76 @@ const server = http.createServer(async (req, res) => {
259
260
  if (l.scope === "global") out.lessons.global.push(rec); else (out.lessons.byAgent[l.scope] ||= []).push(rec);
260
261
  if (rec.project) { (out.lessons.byProject[rec.project] ||= []).push(rec); projSet.add(rec.project); }
261
262
  }
262
- out.lessons.projects = [...projSet].sort();
263
263
 
264
- // per-LLM reliability from turn telemetry: turns, failures (exit!=0), daily fail-rate series
264
+ // per-LLM reliability from turn telemetry, bucketed BY PROJECT (+ a global ALL bucket) so the
265
+ // sidebar's project filter scopes the charts. Each turn carries its own project.
265
266
  const turns = scanTelemetry();
266
- const byAgent = {}; let totalTurns = 0, totalFails = 0; const modelsSeen = new Set();
267
- for (const t of turns) {
268
- const a = (byAgent[t.agent] ||= { agent: t.agent, turns: 0, failures: 0, models: new Set(), lastFailure: null, days: {} });
269
- a.turns++; totalTurns++;
270
- if (t.model) { a.models.add(t.model); if (t.model !== "default") modelsSeen.add(t.model); }
267
+ const relAgg = {}; // scope -> agent -> {turns,failures,models:Set,lastFailure,days}
268
+ const scopeModels = {}; // scope -> Set(model used)
269
+ let totalTurns = 0, totalFails = 0;
270
+ const bumpRel = (scope, t) => {
271
+ const a = ((relAgg[scope] ||= {})[t.agent] ||= { agent: t.agent, turns: 0, failures: 0, models: new Set(), lastFailure: null, days: {} });
272
+ a.turns++;
273
+ if (t.model) { a.models.add(t.model); if (t.model !== "default") (scopeModels[scope] ||= new Set()).add(t.model); }
271
274
  const dk = dayOf(t.ts); const d = dk ? (a.days[dk] ||= { turns: 0, failures: 0 }) : null; if (d) d.turns++;
272
- if (t.exit && t.exit !== 0) { a.failures++; totalFails++; if (d) d.failures++; if (!a.lastFailure || t.ts > a.lastFailure.ts) a.lastFailure = { ts: t.ts, exit: t.exit, project: t.project || "" }; }
275
+ if (t.exit && t.exit !== 0) { a.failures++; if (d) d.failures++; if (!a.lastFailure || t.ts > a.lastFailure.ts) a.lastFailure = { ts: t.ts, exit: t.exit, project: t.project || "" }; }
276
+ };
277
+ for (const t of turns) {
278
+ if (!t.agent) continue;
279
+ totalTurns++; if (t.exit && t.exit !== 0) totalFails++;
280
+ bumpRel(ALL, t);
281
+ if (t.project) { bumpRel(t.project, t); projSet.add(t.project); }
273
282
  }
274
- // lessons-accumulated-over-time per agent scope (relay lessons carry a ts; skip the unstamped older ones)
275
- const lessonDays = {};
276
- for (const l of state.lessons) { const d = dayOf(l.ts); if (!d) continue; (lessonDays[l.scope] ||= {}); lessonDays[l.scope][d] = (lessonDays[l.scope][d] || 0) + 1; }
277
- out.agents = Object.values(byAgent).sort((a, b) => b.turns - a.turns).map(a => {
278
- const days = Object.keys(a.days).sort();
279
- let cum = 0; const ld = lessonDays[a.agent] || {};
280
- return {
281
- agent: a.agent, turns: a.turns, failures: a.failures, failRate: a.turns ? +(a.failures / a.turns).toFixed(3) : 0,
283
+ // lessons-accumulated-over-time per scope -> agent brand -> day (agent-scoped lessons only)
284
+ const lessonAgg = {};
285
+ for (const l of state.lessons) {
286
+ if (l.scope === "global") continue; const d = dayOf(l.ts); if (!d) continue;
287
+ const bump = scope => { (((lessonAgg[scope] ||= {})[l.scope] ||= {})[d]) = (lessonAgg[scope][l.scope][d] || 0) + 1; };
288
+ bump(ALL); const p = projOf(l.by); if (p) bump(p);
289
+ }
290
+ const buildAgents = scope => Object.values(relAgg[scope] || {}).sort((a, b) => b.turns - a.turns).map(a => {
291
+ const days = Object.keys(a.days).sort(); let cum = 0; const ld = (lessonAgg[scope] || {})[a.agent] || {};
292
+ return { agent: a.agent, turns: a.turns, failures: a.failures, failRate: a.turns ? +(a.failures / a.turns).toFixed(3) : 0,
282
293
  lastFailure: a.lastFailure, models: [...a.models],
283
294
  series: {
284
295
  failRate: days.map(d => ({ day: d, turns: a.days[d].turns, failures: a.days[d].failures, rate: a.days[d].turns ? +(a.days[d].failures / a.days[d].turns).toFixed(3) : 0 })),
285
296
  lessons: Object.keys(ld).sort().map(d => ({ day: d, count: (cum += ld[d]) })),
286
- },
287
- };
297
+ } };
288
298
  });
289
299
 
290
- // Scrooge guardrails (per model → per task) + per-model economics from the cached ledger
300
+ // Scrooge guardrails (global per model) + per-model economics from the ledger, bucketed by project
291
301
  let guard = {}; try { guard = JSON.parse(readFileSync(join(homedir(), ".token-scrooge", "lessons.json"), "utf8")) || {}; } catch {}
292
302
  try { const lp = join(homedir(), ".token-scrooge", "calls.jsonl"); const st = statSync(lp); if (st.mtimeMs !== _ledgerCache.mtimeMs) { const rows = readFileSync(lp, "utf8").trim().split("\n").map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(c => c && c.ok); _ledgerCache = { mtimeMs: st.mtimeMs, rows }; } } catch {}
293
- const ledgerByModel = {};
294
- for (const c of _ledgerCache.rows) {
295
- const m = (ledgerByModel[c.model] ||= { calls: 0, ti: 0, to: 0, cost: 0, days: {} });
303
+ const ledAgg = {}; // scope -> model -> {calls,ti,to,cost,days}
304
+ const bumpLed = (scope, c) => {
305
+ const m = ((ledAgg[scope] ||= {})[c.model] ||= { calls: 0, ti: 0, to: 0, cost: 0, days: {} });
296
306
  m.calls++; m.ti += c.tokens_in || 0; m.to += c.tokens_out || 0; m.cost += c.cost_usd || 0;
297
307
  const dk = dayOf(c.ts); if (dk) { const d = (m.days[dk] ||= { cost: 0, ti: 0, to: 0 }); d.cost += c.cost_usd || 0; d.ti += c.tokens_in || 0; d.to += c.tokens_out || 0; }
298
- }
308
+ (scopeModels[scope] ||= new Set()).add(c.model);
309
+ };
310
+ for (const c of _ledgerCache.rows) { if (!c.model) continue; bumpLed(ALL, c); if (c.project) { bumpLed(c.project, c); projSet.add(c.project); } }
311
+
299
312
  const savedOf = (ti, to, cost) => +Math.max(0, ti * 15 / 1e6 + to * 75 / 1e6 - cost).toFixed(2);
300
313
  let totalGuardrails = 0;
301
- const mkModel = (model, g) => {
314
+ const mkModel = (scope, model, g) => {
302
315
  const gcount = Object.values(g || {}).reduce((s, arr) => s + (Array.isArray(arr) ? arr.length : 0), 0);
303
- totalGuardrails += gcount; const lm = ledgerByModel[model];
316
+ if (scope === ALL) totalGuardrails += gcount; // guardrails are global — count once
317
+ const lm = (ledAgg[scope] || {})[model];
304
318
  return { model, guardrails: g || {}, guardrailCount: gcount, calls: lm ? lm.calls : 0, cost_usd: lm ? +lm.cost.toFixed(4) : 0,
305
319
  saved_usd: lm ? savedOf(lm.ti, lm.to, lm.cost) : 0,
306
320
  series: { saved: lm ? Object.keys(lm.days).sort().map(d => ({ day: d, saved: savedOf(lm.days[d].ti, lm.days[d].to, lm.days[d].cost) })) : [] } };
307
321
  };
308
- const modelKeys = new Set([...Object.keys(guard).filter(k => k !== "*"), ...Object.keys(ledgerByModel), ...modelsSeen]);
309
- out.models = [...modelKeys].sort().map(m => mkModel(m, guard[m]));
310
- if (guard["*"]) out.models.unshift(mkModel("∗ all models", guard["*"])); // guardrails that apply to every model
322
+ const buildModels = scope => {
323
+ const keys = new Set(scopeModels[scope] || []); // models used in this scope
324
+ if (scope === ALL) for (const k of Object.keys(guard)) if (k !== "*") keys.add(k); // global view also lists every guardrailed model
325
+ const arr = [...keys].sort().map(m => mkModel(scope, m, guard[m]));
326
+ if (guard["*"]) arr.unshift(mkModel(scope, "∗ all models", guard["*"])); // guardrails that apply to every model
327
+ return arr;
328
+ };
329
+
330
+ out.lessons.projects = [...projSet].sort();
331
+ out.agents = buildAgents(ALL); out.models = buildModels(ALL);
332
+ for (const p of out.lessons.projects) { out.agentsByProject[p] = buildAgents(p); out.modelsByProject[p] = buildModels(p); }
311
333
 
312
334
  out.totals = { lessons: state.lessons.length, guardrails: totalGuardrails, turns: totalTurns, failures: totalFails, failRate: totalTurns ? +(totalFails / totalTurns).toFixed(3) : 0, models: out.models.length };
313
335
  return json(res, 200, out);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.17.5",
3
+ "version": "0.17.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"
package/ui.html CHANGED
@@ -557,20 +557,22 @@ function renderLearnBody(){const el=$('#learnBody');if(!el)return;if(!LEARN){el.
557
557
  else h+=lessons.slice(0,40).map(l=>`<div class="lles"><span class="sc">${esc(l.scope)}</span>${esc(l.text)}</div>`).join('');
558
558
  if(!learnProj)for(const[ag,arr]of Object.entries(L.lessons.byAgent||{}))h+=`<div class="lles"><span class="sc">${esc(ag)}</span>${esc(arr[0].text)}${arr.length>1?` <span class="dim">+${arr.length-1} more</span>`:''}</div>`;
559
559
  h+=`</div>`;
560
- // Per-LLM reliability + progression charts
561
- h+=`<div class="lsec"><h3>Per-LLM reliability</h3>`;
562
- if(!(L.agents||[]).length)h+=`<div class="lempty">no turn telemetry yet</div>`;
563
- for(const a of (L.agents||[])){const fr=(a.failRate*100).toFixed(1);
560
+ // Per-LLM reliability + progression charts — scoped to the selected project (or all)
561
+ const scopeLbl=learnProj?` <span class="dim" style="text-transform:none;letter-spacing:0">· ${esc(learnProj)}</span>`:'';
562
+ const ags=learnProj?(L.agentsByProject[learnProj]||[]):(L.agents||[]);
563
+ h+=`<div class="lsec"><h3>Per-LLM reliability${scopeLbl}</h3>`;
564
+ if(!ags.length)h+=`<div class="lempty">no turn telemetry${learnProj?' for this project':''} yet</div>`;
565
+ for(const a of ags){const fr=(a.failRate*100).toFixed(1);
564
566
  h+=`<div class="lcard"><div class="lc-h">${iconFor(a.agent,14)}<span class="nm">${esc(a.agent)}</span><span class="mut">${a.models.filter(m=>m!=='default').join(', ')||'default'}</span></div>`;
565
567
  h+=`<span class="lstat"><b>${a.turns}</b> turns</span><span class="lstat ${a.failures?'bad':'good'}"><b>${fr}%</b> fail</span>${a.lastFailure?`<span class="lstat">last exit ${a.lastFailure.exit}</span>`:''}`;
566
568
  if((a.series.failRate||[]).length>1)h+=`<div class="lchart"><div class="ltask">fail-rate / day</div>${lcLine(a.series.failRate,'rate',{color:LRED,fill:true})}</div>`;
567
569
  if((a.series.lessons||[]).length>1)h+=`<div class="lchart"><div class="ltask">lessons learned (cumulative)</div>${lcLine(a.series.lessons,'count',{color:LGRN,fill:true})}</div>`;
568
570
  h+=`</div>`;}
569
571
  h+=`</div>`;
570
- // Baked-in guardrails per model + savings chart
571
- h+=`<div class="lsec"><h3>Baked-in improvements <span class="lbadge">auto-injected into prompts</span></h3>`;
572
- const gm=(L.models||[]).filter(m=>m.guardrailCount>0);
573
- if(!gm.length)h+=`<div class="lempty">no model guardrails yet</div>`;
572
+ // Baked-in guardrails per model + savings chart — guardrails are global; savings scoped to the project
573
+ h+=`<div class="lsec"><h3>Baked-in improvements${scopeLbl} <span class="lbadge">auto-injected into prompts</span></h3>`;
574
+ const gm=(learnProj?(L.modelsByProject[learnProj]||[]):(L.models||[])).filter(m=>m.guardrailCount>0);
575
+ if(!gm.length)h+=`<div class="lempty">no guardrailed models${learnProj?' used in this project':''} yet</div>`;
574
576
  for(const m of gm){h+=`<div class="lcard"><div class="lc-h"><span class="nm">${esc(m.model)}</span>${m.saved_usd?`<span class="mut">$${m.saved_usd} saved · ${m.calls} calls</span>`:''}</div>`;
575
577
  for(const[task,arr]of Object.entries(m.guardrails||{})){h+=`<div class="ltask">${esc(task)}</div>`;for(const g of arr)h+=`<div class="lguard">${esc(g)}</div>`;}
576
578
  if((m.series.saved||[]).length>1)h+=`<div class="lchart"><div class="ltask">savings / day</div>${lcBars(m.series.saved,'saved',{color:LGRN})}</div>`;