trantor 0.18.104 → 0.18.105

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.104",
3
+ "version": "0.18.105",
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": {
@@ -23,7 +23,7 @@ import {
23
23
  } from "../lib/classify-failure.mjs";
24
24
  import { capWake, capBcast, pickLessons, composePrompt, contractBase, baseLine } from "./crew-payload.mjs";
25
25
  import {
26
- cardRefs, servedContractCard, carriesWork, parseTurnTokens, parseResetAt, reasonWithBalances, quotaResetAt, PARKING_REASONS,
26
+ cardRefs, servedContractCard, carriesWork, parseTurnTokens, parseResetAt, reasonWithBalances, quotaResetAt, queuedWording, turnDelivered, budgetPause, PARKING_REASONS,
27
27
  senderProjectOf, isLinkedProject, stateSkipReason, isMessageCardTitle, OPEN_CARD_STATUSES,
28
28
  CUT_CHAIN_PARK_MIN, cutChainEvidence, isBoundedPark, askDuringTurn, askedVerdict,
29
29
  } from "../lib/turn-policy.mjs";
@@ -627,6 +627,13 @@ async function notifyAssigners(pairs, text) {
627
627
  async function latestBusEventId() {
628
628
  try { const r = await api("/events?limit=1"); return Number(r?.latest) || 0; } catch { return 0; }
629
629
  }
630
+ // #11860: this seat's direct messages since `seq`, to tell a reply to the assigner from silence.
631
+ async function busMessagesSince(seq) {
632
+ try {
633
+ const r = await api(`/events?by=${encodeURIComponent(SESSION)}&since=${seq}&limit=500`);
634
+ return Array.isArray(r?.events) ? r.events.filter(e => e?.type === "message") : [];
635
+ } catch { return []; }
636
+ }
630
637
  async function busActivitySince(seq) {
631
638
  try {
632
639
  const r = await api(`/events?by=${encodeURIComponent(SESSION)}&since=${seq}&limit=500`);
@@ -1149,9 +1156,10 @@ exit $turn_exit`;
1149
1156
  // Judged on hub reads of the card status; a failed read keeps today's label, never no-delivery.
1150
1157
  let cardStatusAtEnd = "", noDelivery = false, blockedVerdict = "";
1151
1158
  if (!cut && !opts.state && realExit === 0 && effExit === 0 && sessionCard > 0 && cardStatusBefore !== null) {
1152
- let endStatus = null;
1159
+ let endStatus = null, endTask = null;
1153
1160
  try {
1154
1161
  const { task } = await api(`/card?project=${encodeURIComponent(PROJ)}&id=${sessionCard}`);
1162
+ endTask = task;
1155
1163
  endStatus = task ? String(task.status || "") : null;
1156
1164
  const freshNote = (task?.log || []).filter(n => n.by === SESSION && n.ts >= t0
1157
1165
  && !cardLogBefore.some(old => old.ts === n.ts && old.by === n.by && old.text === n.text)).at(-1);
@@ -1170,7 +1178,11 @@ exit $turn_exit`;
1170
1178
  // still IS testing/done: a mid-turn bounce to doing (endStatus doing) reopens the contract.
1171
1179
  const alreadyDelivered = ["testing", "done"].includes(cardStatusBefore)
1172
1180
  && ["testing", "done"].includes(endStatus);
1173
- noDelivery = !newCommit && !moved && !alreadyDelivered && !askVerdict && !blockedVerdict;
1181
+ noDelivery = !alreadyDelivered && !askVerdict && !blockedVerdict && !newCommit && !moved;
1182
+ if (noDelivery) {
1183
+ const events = await busMessagesSince(busSeqBefore);
1184
+ noDelivery = !turnDelivered({ newCommit, moved, session: SESSION, t0, cardLogBefore, cardLog: endTask?.log || [], events, assigners: opts.assigners, seqBefore: busSeqBefore });
1185
+ }
1174
1186
  }
1175
1187
  }
1176
1188
  lastNoDelivery = noDelivery;
@@ -1692,7 +1704,7 @@ async function resolveWakeCard(messages, { session }) {
1692
1704
  }
1693
1705
  // Respect an active backoff: a new message during an outage joins the batch, it does not
1694
1706
  // reset the clock and hammer a CLI that is already failing.
1695
- if (Date.now() < retryAt) { log(`queued — ${pendingWake.length} undelivered, next attempt in ${Math.max(0, Math.round((retryAt - Date.now()) / 1000))}s`); continue; }
1707
+ if (Date.now() < retryAt) { log(queuedWording(retryAt, parkState?.reason, AGENT, pendingWake.length)); continue; }
1696
1708
  await deliverWake();
1697
1709
  log("parked — waiting for the next message");
1698
1710
  }
@@ -1761,7 +1773,7 @@ async function resolveWakeCard(messages, { session }) {
1761
1773
  await releaseDutyNudgeClaims({ ids: skippedNudgeIds, statePath: DUTY_NUDGE_STATE, owner: dutyOwner });
1762
1774
  }
1763
1775
  const claimedIds = new Set(dutyPlan.items.map(item => item.id));
1764
- const wakeForTurn = DUTY_NUDGES
1776
+ let wakeForTurn = DUTY_NUDGES
1765
1777
  ? wake.filter(message => {
1766
1778
  const escalation = dutyEscalations([message])[0];
1767
1779
  return !escalation || claimedIds.has(escalation.id);
@@ -1773,6 +1785,21 @@ async function resolveWakeCard(messages, { session }) {
1773
1785
  log("duty escalation already nudged or reserved by another turn — consumed without a duplicate model wake");
1774
1786
  return;
1775
1787
  }
1788
+ // #11864: the pause lifts when its card moves on, and a newer contract for another card beats
1789
+ // the stale messages that still cite the paused one.
1790
+ if (budgetStop) {
1791
+ const status = await api(`/card?project=${encodeURIComponent(PROJ)}&id=${budgetStop.card}`).then(r => String(r?.task?.status || "")).catch(() => "");
1792
+ const bp = budgetPause({ paused: budgetStop, status, messages: wakeForTurn, session: SESSION });
1793
+ if (bp.action === "lift") {
1794
+ log(`#${budgetStop.card} is ${status} — token-budget pause lifted`);
1795
+ budgetStop = null; budgetRetryAt = 0; cardTokens = 0;
1796
+ } else if (bp.action === "bind") {
1797
+ log(`#${budgetStop.card} is paused over its token budget — serving the newer contract for #${bp.card}, dropping ${bp.drop.length} stale message(s)`);
1798
+ wakeForTurn = wakeForTurn.filter(m => !bp.drop.includes(m));
1799
+ pendingWake = pendingWake.filter(m => !bp.drop.includes(m));
1800
+ savePending(pendingWake, pendingBcast);
1801
+ }
1802
+ }
1776
1803
  const wakeCapped = capWake(wakeForTurn);
1777
1804
  const bcastCapped = capBcast(pendingBcast);
1778
1805
  const wakeText = wakeCapped.text
@@ -173,8 +173,11 @@ const RESET_ABS_RE = new RegExp(
173
173
  "i",
174
174
  );
175
175
  const RESET_REL_RE = /try again in\s+(\d+)\s*(second|minute|hour|day)s?/i;
176
+ // A fixed rolling window ("5-hour usage limit") names no timestamp; the error time + N hours is the bound.
177
+ const RESET_WINDOW_RE = /\b(\d{1,3})[-\s]hours?\s+(?:usage\s+)?limit/i;
176
178
 
177
179
  /// The epoch ms at which this seat may be retried, or 0 when the output named no time.
180
+ /// A "<N>-hour usage limit" window counts as N hours from `now`, the moment of the error.
178
181
  export function parseResetAt(text, now = Date.now()) {
179
182
  const s = String(text || "");
180
183
 
@@ -191,9 +194,50 @@ export function parseResetAt(text, now = Date.now()) {
191
194
  if (unit) return now + Number(rel[1]) * unit;
192
195
  }
193
196
 
197
+ const win = RESET_WINDOW_RE.exec(s);
198
+ if (win && Number(win[1]) > 0) return now + Number(win[1]) * 3600e3;
199
+
194
200
  return 0;
195
201
  }
196
202
 
203
+ /// #11860: a design, research or drill step legitimately ends with no commit and the card unmoved.
204
+ /// It still delivered when this seat wrote a note on its OWN card during the turn, or messaged
205
+ /// the contract's sender directly. Reading and saying nothing stays no-delivery.
206
+ export function turnDelivered({ newCommit, moved, session, t0, cardLogBefore = [], cardLog = [], events = [], assigners = [], seqBefore = 0 }) {
207
+ if (newCommit || moved) return true;
208
+ const ownNote = (cardLog || []).some(n => n && n.by === session && n.ts >= t0
209
+ && !(cardLogBefore || []).some(old => old.ts === n.ts && old.by === n.by && old.text === n.text));
210
+ if (ownNote) return true;
211
+ const owed = new Set((assigners || []).map(a => a && a.from).filter(f => f && f !== session));
212
+ return (events || []).some(e => e && e.type === "message" && e.by === session
213
+ && Number(e.id) > seqBefore && owed.has(e.toSession));
214
+ }
215
+
216
+ /// #11864: what a card's token-budget pause means for this wake batch. `lift` once the paused card
217
+ /// is in testing/done/failed; `bind` when the batch also holds a newer contract for ANOTHER card
218
+ /// (the turn serves that card, and `drop` are the stale messages for the paused one); else `hold`.
219
+ /// `none` when nothing in the batch cites the paused card.
220
+ export function budgetPause({ paused, status = "", messages = [], session = "" }) {
221
+ if (!paused || !(paused.card > 0)) return { action: "none" };
222
+ if (["testing", "done", "failed"].includes(status)) return { action: "lift" };
223
+ const all = (Array.isArray(messages) ? messages : []).filter(Boolean);
224
+ if (!all.some((m) => cardRefs(m.text).includes(paused.card) || Number(m.card) === paused.card)) return { action: "none" };
225
+ const own = (m) => (Number(m.card) > 0 ? Number(m.card) : (assignedCardRef(m.text) || cardRefs(m.text)[0] || 0));
226
+ const direct = newestLast(all.filter((m) => !session || m.to === session));
227
+ const fresh = direct.at(-1);
228
+ const next = fresh ? own(fresh) : 0;
229
+ if (!(next > 0) || next === paused.card) return { action: "hold" };
230
+ const drop = all.filter((m) => m !== fresh && cardRefs(m.text).includes(paused.card) && !cardRefs(m.text).includes(next));
231
+ return { action: "bind", card: next, drop };
232
+ }
233
+
234
+ /// The "queued" log line for an undelivered batch behind a park: a clock time when the retry is
235
+ /// bounded, a `trantor up` pointer when it is not.
236
+ export function queuedWording(retryAt, reason, agent, undelivered, now = Date.now()) {
237
+ if (retryAt >= Number.MAX_SAFE_INTEGER / 2) return `parked (${reason || "exhausted"}) until \`trantor up ${agent}\` — ${undelivered} undelivered`;
238
+ return `queued — ${undelivered} undelivered, next attempt at ${new Date(retryAt).toLocaleTimeString()} (in ${Math.max(0, Math.round((retryAt - now) / 1000))}s)`;
239
+ }
240
+
197
241
  /// #6131: a qwen seat whose token plan is spent does not error — it stalls and returns nothing, so
198
242
  /// the runner classified it `empty-output` and kept the ladder running against a wall. A silent
199
243
  /// turn on a seat whose own balance row reads spent IS exhaustion, and parks like one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.104",
3
+ "version": "0.18.105",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"