trantor 0.18.71 → 0.18.72

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.71",
3
+ "version": "0.18.72",
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": {
@@ -89,6 +89,10 @@ export function verdictFor(realExit, effExit, emptyOutput, ownText, emptyTurn =
89
89
  : "classified success because exit 0 with CLI output";
90
90
  if (realExit === 0 && effExit === 1) {
91
91
  if (emptyOutput) return "classified empty-output because exit 0 with no output on either stream";
92
+ // #8778: the exit-0 escalation must not relabel a provider-side rejection as "auth" — codex
93
+ // prints the chatgpt-backend 401 and exits 0, so the real specimen rode this exact path.
94
+ const pr = providerRejected(String(ownText || ""));
95
+ if (pr) return `classified ${pr.reason} because ${pr.matched}`;
92
96
  const m = AUTH_MARKER_RE.exec(String(ownText || ""));
93
97
  return `classified auth because ${m ? m[0] : "auth marker"} in the CLI's own short output`;
94
98
  }
@@ -96,6 +100,26 @@ export function verdictFor(realExit, effExit, emptyOutput, ownText, emptyTurn =
96
100
  return `classified ${reason} because ${matched}`;
97
101
  }
98
102
 
103
+ // ---- the provider-rejected 401 (#8778) ---------------------------------------------------------
104
+ // chatgpt.com/backend-api is the ChatGPT-SUBSCRIPTION backend codex hits while logged in with no
105
+ // API key stored, so a 401 naming it is OpenAI rejecting its own server-side key — the specimen's
106
+ // sk-svcacct key existed in no env file, keychain or process env on the machine.
107
+ const REQUEST_ID_RE = /request id:\s*([a-z0-9][a-z0-9-]*)/i;
108
+
109
+ /** The #8778 rule: a 401 whose url is the chatgpt.com/backend-api endpoint classifies
110
+ * provider-rejected — the verdict names the request id and steers to waiting or the OpenAI
111
+ * account, never a re-login. Returns null for any other shape, so a 401 from an API-key
112
+ * endpoint (or one naming no endpoint) stays "auth". */
113
+ export function providerRejected(errText) {
114
+ const t = String(errText || "");
115
+ if (!/\b401\b/.test(t) || !/chatgpt\.com\/backend-api/i.test(t)) return null;
116
+ const rid = (t.match(REQUEST_ID_RE) || [])[1] || "";
117
+ return {
118
+ reason: "provider-rejected",
119
+ matched: `401 from https://chatgpt.com/backend-api${rid ? ` (request id ${rid})` : ""} — OpenAI rejected its own subscription key server-side; wait or check the OpenAI account, logging in again will not help`,
120
+ };
121
+ }
122
+
99
123
  /** Classify one failed turn. Returns { reason, matched } — matched is the evidence excerpt the
100
124
  * runner logs, so the next misclassification is diagnosable from the seat log alone. `cut` is the
101
125
  * runner's box/stall marker: 137 reads as the sweep's SIGKILL only under it — without the marker
@@ -118,9 +142,13 @@ export function classifyFailure(exit, errText, emptyOutput = false, emptyTurn =
118
142
  // #5684: a provider BACKEND failure is not quota — it wants retry/swap, not a window wait.
119
143
  // The specimen (#5683): codex's "unexpected status 404 Not Found … /responses/compact" was
120
144
  // labelled "exhausted" and the operator was advised to wait out a window that did not exist.
121
- // 401/403/429 deliberately fall through to the auth/exhausted branches below.
145
+ // 401/403/429 deliberately fall through to the provider-rejected/auth/exhausted branches below.
122
146
  let m = t.match(/unexpected status (?:404|408|410|5\d\d)|internal server error|bad gateway|service unavailable|gateway time.?out|econnrefused|connection refused|socket hang ?up|network is unreachable/);
123
147
  if (m) return { reason: "backend-error", matched: m[0] };
148
+ // #8778: ranked ABOVE the quota/auth patterns — the specimen text itself carries an "API key"
149
+ // phrase, and either pattern reading first would relabel the same provider-side rejection.
150
+ const pr = providerRejected(String(errText || ""));
151
+ if (pr) return pr;
124
152
  // "reached your … limit" / "usage limit" catch the subscription CLIs (Claude's "You've reached
125
153
  // your Fable 5 limit"), which say nothing about quota or credits and would otherwise read as a crash.
126
154
  m = t.match(/quota|payment required|402|429|too many requests|rate.?limit|usage limit|exceeded your|reached your [^.\n]*limit|insufficient (?:credits?|funds)|out of (?:credits?|quota)/);
@@ -170,11 +170,10 @@ export function deriveState({ project, seat, card, worktree, handoffText, cardTi
170
170
  now: Number.isInteger(now) ? now : Math.floor(Date.now() / 1000),
171
171
  by: author,
172
172
  files: deriveFiles(worktree),
173
- // This build is RECONSTRUCTION, not a seat asserting completion (#8068): a ✅ bullet in the
174
- // predecessor's handoff records what it reported done. The evidence rule guards a live claim,
175
- // and applying it here would drop the handoff's items on the floor. Credit is withheld the way
176
- // the contract already says — deriveFiles marks every path verified:false and `verify` stays
177
- // empty — so the successor must still re-earn evidence before anything NEW moves to done.
173
+ // RECONSTRUCTION, not a live claim (#8068): a ✅ bullet records what the predecessor reported
174
+ // done, so the arrival-in-done gate must not fire on this path — CONTRACT-state.md, "Derive".
175
+ // Credit is still withheld (verified:false everywhere), so the successor re-earns evidence
176
+ // before anything NEW moves to done.
178
177
  reconstructing: true,
179
178
  };
180
179
 
@@ -277,12 +277,10 @@ export async function runStep({
277
277
  // Per attempt, not per step (#7226): a cut on the first attempt belongs to that attempt's row.
278
278
  let stepCut = cut === true;
279
279
 
280
- // §7.3's fallback ladder: a MALFORMED patch earns exactly one retry, with the rejection message
281
- // as the observation. An EVIDENCE rejection is not retried in-turn — the code was wrong, not the
282
- // grammar, and the seat needs a whole step to fix the code (§4.8).
283
- // Where HEAD sat before this step ran anything (#8069). Captured ONCE, outside the retry loop:
284
- // a malformed-output retry re-runs the CLI, and commits the first attempt already made are still
285
- // this step's work — re-reading HEAD per attempt would silently drop them.
280
+ // Two seams, both in CONTRACT-state.md: (1) §7.3's ladder — a MALFORMED patch earns exactly one
281
+ // retry with the rejection as the observation; an EVIDENCE rejection is not retried in-turn,
282
+ // the code was wrong, not the grammar (#8068). (2) HEAD captured ONCE, outside the retry loop:
283
+ // the first attempt's commits are still this step's work (#8069).
286
284
  const headAtStart = (D.gitHead || gitHead)(cwd);
287
285
 
288
286
  for (;;) {
@@ -91,12 +91,9 @@ export function gitHead(cwd) {
91
91
  return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
92
92
  }
93
93
 
94
- /** Paths changed by the COMMITS made since `sha`, NUL-separated (#8069). The two commands §4.8 named
95
- * — `git status --porcelain` and `git diff --name-only HEAD` — both measure the working tree against
96
- * HEAD, so a seat that commits moves HEAD along with its work and both come back empty. That is not a
97
- * corner case: every seat is told to commit early and often so a cut cannot erase it, which made tier 1
98
- * blind on the ordinary flow. A card that rewrote 8,000 lines across 4 commits recorded files:{}.
99
- * Degrades to [] on any git error, exactly like the other readers — recovery never throws. */
94
+ /** Paths changed by the COMMITS made since `sha`, NUL-separated (#8069). The §4.8 status/diff pair
95
+ * measures the tree against HEAD, so a seat that commits (the ordinary flow) reads empty from
96
+ * both — CONTRACT-state.md, "Tier 1". Degrades to [] on any git error, like the other readers. */
100
97
  export function gitCommittedSince(sha, cwd) {
101
98
  if (!sha || !cwd) return [];
102
99
  const out = git(["diff", "--name-only", "-z", `${sha}..HEAD`], cwd);
@@ -49,21 +49,16 @@ export function hasEvidence(state, item, ctx = {}) {
49
49
  return paths.every(p => ({ ...(state.files[p] || {}), ...(ctxFiles[p] || {}) }).verified === true);
50
50
  }
51
51
 
52
- /** The evidence gate for an item ARRIVING in `done`, by whichever op put it there (#8068). Returns the
53
- * rejection, or null when the item may land. `move → done` had this inline and `add … list:"done"` had
54
- * nothing at all, so a seat that completed an item in the turn it did the work — the ordinary flow —
55
- * walked past the verified-done rule entirely. Both doors now lead through this one check; `at` is the
56
- * only thing that differs, so the seat's rejection still names the op it actually wrote. */
52
+ /** The evidence gate for an item ARRIVING in `done`, by whichever op put it there (#8068). Returns
53
+ * the rejection, or null when the item may land. Both doors — `move → done` and
54
+ * `add … list:"done"` — lead through this one check (CONTRACT-state.md, "Schema and validation");
55
+ * only `at` differs, so the rejection names the op the seat actually wrote. */
57
56
  function arrivalInDone(state, item, ctx, at) {
58
57
  if (!item || hasEvidence(state, item, ctx)) return null;
59
- // The one exception, and it is reconstruction rather than assertion: derive.mjs rebuilds a
60
- // WorkingState from a predecessor's prose handoff, where a ✅ bullet is a RECORD of what was
61
- // reported done, not this seat claiming it now. Gating that would delete the handoff's content —
62
- // #6528's failure exactly — and the rule it would be enforcing is already kept another way:
63
- // CONTRACT-state.md, "a derived state carries no credit … the successor must re-earn its evidence
64
- // before anything moves to done". The items arrive uncredited, so the successor still cannot ride
65
- // route (b) on them. `ctx` is harness-supplied and a patch cannot write it (§3.0 write matrix),
66
- // so a seat cannot set this for itself — which is the only reason a flag is safe here at all.
58
+ // The one exception is RECONSTRUCTION, not assertion (#8068): derive.mjs's ✅ bullets are records
59
+ // of what the predecessor reported done, and gating them would delete the handoff's content
60
+ // (#6528's failure). The skipped rule is kept another way (CONTRACT-state.md, "Derive"); ctx is
61
+ // harness-supplied, so a seat cannot set this flag for itself.
67
62
  if (ctx.reconstructing === true) return null;
68
63
  // The split that makes §4.8's one-retry bound a fact: a gate that ran and failed looks
69
64
  // exactly like a gate that never ran, unless the driver says which happened.
@@ -207,8 +207,9 @@ export function quotaResetAt(rows, now = Date.now()) {
207
207
  }
208
208
 
209
209
  /// Seats that park rather than retry. A backend error is the provider having a bad minute and the
210
- /// ladder is exactly right for it; a spent plan or a rejected key will not fix itself on a timer.
211
- export const PARKING_REASONS = new Set(["exhausted", "auth"]);
210
+ /// ladder is exactly right for it; a spent plan, a rejected key, or a provider-side key rejection
211
+ /// (#8778: the chatgpt-backend 401 on a seat whose login is valid) will not fix itself on a timer.
212
+ export const PARKING_REASONS = new Set(["exhausted", "auth", "provider-rejected"]);
212
213
 
213
214
  /// #6228: the sender's home project, by the same "name suffix after the last colon" convention
214
215
  /// every crew/orch identity is minted with (isRunnerSession in crew-runner.mjs uses the mirror
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.71",
3
+ "version": "0.18.72",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"