trantor 0.18.103 → 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/core.mjs +4 -1
- package/bin/crew-runner.mjs +37 -7
- 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/core.mjs
CHANGED
|
@@ -4,6 +4,9 @@ import { spawnSync } from "node:child_process";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { STATE_FLAGS } from "../../lib/state/flags.mjs";
|
|
6
6
|
|
|
7
|
+
// the card limits the runner reads (#11559 budget, #11855 context cap), tunable at `trantor up`
|
|
8
|
+
export const RUNNER_LIMIT_FLAGS = ["TRANTOR_CARD_TOKEN_BUDGET", "TRANTOR_CARD_CONTEXT_CAP", "TRANTOR_CARD_CAP_MIN_STEPS", "TRANTOR_CARD_CAP_MAX_HANDOFFS"];
|
|
9
|
+
|
|
7
10
|
export const ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
8
11
|
|
|
9
12
|
export function commandExists(name, env = process.env) {
|
|
@@ -149,7 +152,7 @@ export function gridColumns(size) {
|
|
|
149
152
|
// in lib/state/flags.mjs beside the resolver: the launcher env stays the forwarder (the runner boots
|
|
150
153
|
// before any file is sourced), and #7159 lets the runner fall back to ~/.agent-bus/.env itself, so a
|
|
151
154
|
// flag documented there finally arms the seat that reads it.
|
|
152
|
-
const FORWARDED_ENV = STATE_FLAGS;
|
|
155
|
+
const FORWARDED_ENV = [...STATE_FLAGS, ...RUNNER_LIMIT_FLAGS];
|
|
153
156
|
|
|
154
157
|
export function runnerCommand(ctx, agent, model = "", effort = null, persona = agent) {
|
|
155
158
|
const forwarded = FORWARDED_ENV
|
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";
|
|
@@ -250,7 +250,9 @@ const CLI = {
|
|
|
250
250
|
// kimi-code (successor to kimi-cli) has no --print (-p alone is non-interactive), REJECTS
|
|
251
251
|
// --yolo in prompt mode (prompt mode auto-approves tools), and emits session_-prefixed ids.
|
|
252
252
|
kimi: { first: `kimi{M} -p "$(cat {P})" < /dev/null`,
|
|
253
|
-
|
|
253
|
+
// -S resumes; kimi-code 0.34 silently ignores -r (its own exit hint still prints -r) and
|
|
254
|
+
// opens a new session, so every kimi turn ran cold with no history
|
|
255
|
+
next: `kimi{M} -S {SID} -p "$(cat {P})" < /dev/null`, mflag: " --model ", sid: /To resume this session:\s*kimi (?:-r|-S|--session) (\S+)/ },
|
|
254
256
|
// #6154: `run -c` resumes the globally last session on this machine, any project's, and its stored
|
|
255
257
|
// directory becomes the path root. So every spawn pins --dir and a resume pins -s (ocSid below).
|
|
256
258
|
deepseek: { first: `opencode run --dir {DIR}{M} "$(cat {P})"`,
|
|
@@ -625,6 +627,13 @@ async function notifyAssigners(pairs, text) {
|
|
|
625
627
|
async function latestBusEventId() {
|
|
626
628
|
try { const r = await api("/events?limit=1"); return Number(r?.latest) || 0; } catch { return 0; }
|
|
627
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
|
+
}
|
|
628
637
|
async function busActivitySince(seq) {
|
|
629
638
|
try {
|
|
630
639
|
const r = await api(`/events?by=${encodeURIComponent(SESSION)}&since=${seq}&limit=500`);
|
|
@@ -1051,7 +1060,8 @@ exit $turn_exit`;
|
|
|
1051
1060
|
if (env.turn) log(`state step: ${describeTurn(env.turn)} · ${costLine(env.cost)}`);
|
|
1052
1061
|
else log(`\x1b[33mstate step: no TurnResult — ${env.error}\x1b[0m`);
|
|
1053
1062
|
}
|
|
1054
|
-
|
|
1063
|
+
// kimi prints its resume hint on stderr, so a stdout-only match never captured its sid
|
|
1064
|
+
if (cli.sid) { const all = [...`${r.stdout || ""}\n${ownOut}`.matchAll(new RegExp(cli.sid.source, "g"))]; if (all.length) sid = all.at(-1)[1]; }
|
|
1055
1065
|
// #6154: the opencode family prints no sid on stdout — the id comes from opencode's own DB,
|
|
1056
1066
|
// keyed by the worktree the session was created in. Fail-open: nothing found leaves sid empty,
|
|
1057
1067
|
// and the next turn starts fresh rather than resuming whatever other project ran last.
|
|
@@ -1146,9 +1156,10 @@ exit $turn_exit`;
|
|
|
1146
1156
|
// Judged on hub reads of the card status; a failed read keeps today's label, never no-delivery.
|
|
1147
1157
|
let cardStatusAtEnd = "", noDelivery = false, blockedVerdict = "";
|
|
1148
1158
|
if (!cut && !opts.state && realExit === 0 && effExit === 0 && sessionCard > 0 && cardStatusBefore !== null) {
|
|
1149
|
-
let endStatus = null;
|
|
1159
|
+
let endStatus = null, endTask = null;
|
|
1150
1160
|
try {
|
|
1151
1161
|
const { task } = await api(`/card?project=${encodeURIComponent(PROJ)}&id=${sessionCard}`);
|
|
1162
|
+
endTask = task;
|
|
1152
1163
|
endStatus = task ? String(task.status || "") : null;
|
|
1153
1164
|
const freshNote = (task?.log || []).filter(n => n.by === SESSION && n.ts >= t0
|
|
1154
1165
|
&& !cardLogBefore.some(old => old.ts === n.ts && old.by === n.by && old.text === n.text)).at(-1);
|
|
@@ -1167,7 +1178,11 @@ exit $turn_exit`;
|
|
|
1167
1178
|
// still IS testing/done: a mid-turn bounce to doing (endStatus doing) reopens the contract.
|
|
1168
1179
|
const alreadyDelivered = ["testing", "done"].includes(cardStatusBefore)
|
|
1169
1180
|
&& ["testing", "done"].includes(endStatus);
|
|
1170
|
-
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
|
+
}
|
|
1171
1186
|
}
|
|
1172
1187
|
}
|
|
1173
1188
|
lastNoDelivery = noDelivery;
|
|
@@ -1689,7 +1704,7 @@ async function resolveWakeCard(messages, { session }) {
|
|
|
1689
1704
|
}
|
|
1690
1705
|
// Respect an active backoff: a new message during an outage joins the batch, it does not
|
|
1691
1706
|
// reset the clock and hammer a CLI that is already failing.
|
|
1692
|
-
if (Date.now() < retryAt) { log(
|
|
1707
|
+
if (Date.now() < retryAt) { log(queuedWording(retryAt, parkState?.reason, AGENT, pendingWake.length)); continue; }
|
|
1693
1708
|
await deliverWake();
|
|
1694
1709
|
log("parked — waiting for the next message");
|
|
1695
1710
|
}
|
|
@@ -1758,7 +1773,7 @@ async function resolveWakeCard(messages, { session }) {
|
|
|
1758
1773
|
await releaseDutyNudgeClaims({ ids: skippedNudgeIds, statePath: DUTY_NUDGE_STATE, owner: dutyOwner });
|
|
1759
1774
|
}
|
|
1760
1775
|
const claimedIds = new Set(dutyPlan.items.map(item => item.id));
|
|
1761
|
-
|
|
1776
|
+
let wakeForTurn = DUTY_NUDGES
|
|
1762
1777
|
? wake.filter(message => {
|
|
1763
1778
|
const escalation = dutyEscalations([message])[0];
|
|
1764
1779
|
return !escalation || claimedIds.has(escalation.id);
|
|
@@ -1770,6 +1785,21 @@ async function resolveWakeCard(messages, { session }) {
|
|
|
1770
1785
|
log("duty escalation already nudged or reserved by another turn — consumed without a duplicate model wake");
|
|
1771
1786
|
return;
|
|
1772
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
|
+
}
|
|
1773
1803
|
const wakeCapped = capWake(wakeForTurn);
|
|
1774
1804
|
const bcastCapped = capBcast(pendingBcast);
|
|
1775
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.
|