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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/crew-runner.mjs +32 -5
- package/lib/turn-policy.mjs +44 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
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": {
|
package/bin/crew-runner.mjs
CHANGED
|
@@ -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 = !
|
|
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(
|
|
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
|
-
|
|
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
|
package/lib/turn-policy.mjs
CHANGED
|
@@ -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.
|