trantor 0.18.16 → 0.18.17

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.18.16",
3
+ "version": "0.18.17",
4
4
  "description": "Trantor \u2014 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": {
@@ -234,15 +234,23 @@ let predecessorSid = null;
234
234
  step("S4 · handoff machine: warn → arm → fire → WRITTEN → successor claims → RECAPPED");
235
235
  {
236
236
  // A turn that uses a tool: the heartbeat is PostToolUse, so only tool use can arm the baton.
237
- const raw = await socketRequest({ id: "trantor:agent.prompt", method: "agent.prompt", params: {
238
- target: pane, text: "Use the Bash tool to run exactly `pwd`, then reply with just DONE-S4." } });
239
- if (JSON.parse(raw).result?.type !== "agent_prompted") FAIL("S4 prompt accepted", raw.slice(0, 100));
240
- const handoffFile = await waitFor("handoff written", () => {
237
+ // The heartbeat is PostToolUse: only a REAL tool call can arm the baton. Models sometimes
238
+ // answer without the tool (observed: run 2 of 3 on 2026-08-30 1-in-3 prompt fragility,
239
+ // not a seam), so ask, verify tool use in the transcript, and re-ask up to twice.
240
+ const findHandoff = () => {
241
241
  try {
242
242
  const f = readdirSync(join(bus, "handoffs")).find(x => x.startsWith(`${projectName}-`) && x.endsWith(".json"));
243
243
  return f ? join(bus, "handoffs", f) : null;
244
244
  } catch { return null; }
245
- }, { timeoutMs: 180_000, everyMs: 2_000 });
245
+ };
246
+ let handoffFile = null;
247
+ for (let attempt = 1; attempt <= 3 && !handoffFile; attempt++) {
248
+ const raw = await socketRequest({ id: "trantor:agent.prompt", method: "agent.prompt", params: {
249
+ target: pane, text: `You MUST call the Bash tool now and run exactly: pwd — do not answer without calling it. Then reply with just DONE-S4-${attempt}.` } });
250
+ if (JSON.parse(raw).result?.type !== "agent_prompted") { FAIL("S4 prompt accepted", raw.slice(0, 100)); break; }
251
+ handoffFile = await waitFor("handoff written", findHandoff, { timeoutMs: 120_000, everyMs: 2_000 });
252
+ if (!handoffFile) console.log(` ${D}attempt ${attempt}: no handoff yet — re-asking with the tool requirement${R}`);
253
+ }
246
254
  if (!handoffFile) {
247
255
  FAIL("the armed baton fired and WROTE a handoff at the turn boundary");
248
256
  } else {
package/lib/balances.mjs CHANGED
@@ -51,7 +51,15 @@ export const ADAPTERS = [
51
51
  const win = (w, name) => (w && w.utilization != null)
52
52
  ? { name, usedPct: Math.round(w.utilization), resetsAt: w.resets_at || null, locked: w.locked_reason || null }
53
53
  : null;
54
- return { windows: [win(d.five_hour, "5h"), win(d.seven_day, "7d")].filter(Boolean) };
54
+ // Model-scoped weekly limits ride limits[] (kind "weekly_scoped", scope.model.display_name
55
+ // e.g. "Fable") — Orca shows these as their own segment ("36% used Fable") and so do we.
56
+ // Captured live 2026-08-30: session/weekly_all in limits[] duplicate five_hour/seven_day,
57
+ // so only the scoped entries add information.
58
+ const scoped = (Array.isArray(d.limits) ? d.limits : [])
59
+ .filter((l) => l && l.kind === "weekly_scoped" && l.percent != null && l.scope?.model?.display_name)
60
+ .map((l) => ({ name: String(l.scope.model.display_name), usedPct: Math.round(l.percent),
61
+ resetsAt: l.resets_at || null, locked: null, scoped: true }));
62
+ return { windows: [win(d.five_hour, "5h"), win(d.seven_day, "7d"), ...scoped].filter(Boolean) };
55
63
  },
56
64
  },
57
65
 
@@ -168,17 +176,51 @@ export async function fetchBalances(env = process.env, opts = {}) {
168
176
  catch (e) { return { ...base, ok: false, error: String(e?.message || e) }; }
169
177
  });
170
178
  const rows = (await Promise.all(jobs)).filter(Boolean);
171
- // Codex has no balance API to query it authenticates by `codex login` and bills a
172
- // subscription. The fleet list must still show it, honestly, or the header reads as if the
173
- // seat does not exist. Evidence of configuration is the login artifact, not an env key.
174
- if (!only || only.has("codex") || only.has("openai")) {
179
+ // Codex: OpenAI publishes no public balance API, but the Codex CLI's OWN token can read the
180
+ // ChatGPT backend's usage windows the same source Orca's footer reads (#5570; verified live
181
+ // 2026-08-30: plan_type + primary/secondary_window {used_percent, reset_at}). Evidence of
182
+ // configuration is the login artifact (~/.codex/auth.json), not an env key. The token never
183
+ // leaves the process; only percentages are reported. If the endpoint is unreachable, fall
184
+ // back to the honest subscription row this used to be — never an error row for a seat that
185
+ // bills flat.
186
+ // Same profile gate as every adapter (latent bug from the v6 static row, surfaced 2026-08-30:
187
+ // `!only ||` made a codex row appear with NO profile at all — "better empty than wrong").
188
+ if (only && (only.has("codex") || only.has("openai"))) {
175
189
  try {
176
- const { existsSync } = await import("node:fs");
190
+ const { existsSync, readFileSync } = await import("node:fs");
177
191
  const { join } = await import("node:path");
178
192
  const { homedir } = await import("node:os");
179
- if (existsSync(join(homedir(), ".codex", "auth.json"))) {
180
- rows.push({ provider: "codex", label: "Codex", kind: "subscription", via: "codex login",
181
- ok: true, plan: "OpenAI subscription", note: "no balance API flat subscription" });
193
+ const authPath = join(homedir(), ".codex", "auth.json");
194
+ if (existsSync(authPath)) {
195
+ const base = { provider: "codex", label: "Codex", via: "codex login", ok: true };
196
+ try {
197
+ const tok = JSON.parse(readFileSync(authPath, "utf8"))?.tokens || {};
198
+ if (!tok.access_token) throw new Error("no codex token");
199
+ const r = await fetch("https://chatgpt.com/backend-api/wham/usage", {
200
+ headers: {
201
+ authorization: `Bearer ${tok.access_token}`,
202
+ "ChatGPT-Account-Id": tok.account_id || "",
203
+ "User-Agent": "codex-cli", "OpenAI-Beta": "codex-1", originator: "Codex Desktop",
204
+ },
205
+ signal: AbortSignal.timeout(8000),
206
+ });
207
+ if (!r.ok) throw new Error(`wham/usage ${r.status}`);
208
+ const d = await r.json();
209
+ const win = (w, fallbackName) => {
210
+ if (!w || w.used_percent == null) return null;
211
+ const secs = Number(w.limit_window_seconds) || 0;
212
+ const name = secs === 604800 ? "7d" : secs > 0 && secs <= 21600 ? "5h"
213
+ : secs > 0 ? `${Math.round(secs / 3600)}h` : fallbackName;
214
+ return { name, usedPct: Math.round(w.used_percent),
215
+ resetsAt: w.reset_at ? Number(w.reset_at) * 1000 : null, locked: null };
216
+ };
217
+ const windows = [win(d.rate_limit?.primary_window, "5h"), win(d.rate_limit?.secondary_window, "7d")].filter(Boolean);
218
+ if (!windows.length) throw new Error("no usage windows in response");
219
+ rows.push({ ...base, kind: "windows", plan: d.plan_type ? `ChatGPT ${d.plan_type}` : "OpenAI subscription", windows });
220
+ } catch {
221
+ rows.push({ ...base, kind: "subscription",
222
+ plan: "OpenAI subscription", note: "usage endpoint unreachable — flat subscription" });
223
+ }
182
224
  }
183
225
  } catch { /* no fs access → no row, never an error */ }
184
226
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.16",
3
+ "version": "0.18.17",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"