claudish 7.51.0 → 7.53.0

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/bin/claudish.cjs CHANGED
@@ -3,8 +3,38 @@
3
3
  // Launcher script: checks for Bun runtime before starting claudish.
4
4
  // Claudish uses Bun-specific APIs (bun:ffi for TUI, Bun.spawn, etc.)
5
5
  // so it cannot run under Node.js directly.
6
+ //
7
+ // ── Why this is spawn() and not spawnSync() ──────────────────────────────────
8
+ //
9
+ // This process is a THIN WRAPPER around the real CLI, and it is the process
10
+ // every caller's signal actually lands on. It used to run the child with
11
+ // `spawnSync(bun, ..., { stdio: "inherit" })`, which made the whole tree
12
+ // unkillable in a way that looked like the child was ignoring signals:
13
+ //
14
+ // · `spawnSync` blocks the event loop, so this process cannot forward
15
+ // anything while the child runs.
16
+ // · `stdio: "inherit"` gives the Bun child THIS process's fd 0/1/2. When a
17
+ // caller pipes our stdout, the Bun child holds the write end of that pipe.
18
+ // · So SIGTERM killed only this launcher. The Bun child was ORPHANED, not
19
+ // killed — it kept running, kept billing, and kept the caller's pipe open,
20
+ // writing into it long after the caller had given up.
21
+ //
22
+ // Measured 2026-08-15 with a repro of this exact topology: SIGTERM to the
23
+ // launcher pid leaked 330 B of post-kill output, and so did SIGKILL — because
24
+ // the problem is signalling the wrong process, not a stubborn one. It cost a
25
+ // real `team` run: a model declared TIMEOUT with 0 B wrote a complete 40,699 B
26
+ // answer into the response file 375 s later, and was scored as empty.
27
+ //
28
+ // Now: async spawn, forward the signals a supervisor actually sends, and exit
29
+ // with the child's own status. A caller that terminates `claudish` terminates
30
+ // claudish.
31
+ //
32
+ // This does NOT cover being SIGKILLed ourselves — nothing running in this
33
+ // process can. Callers that need a hard guarantee should spawn claudish
34
+ // `detached` and signal the process GROUP, which reaches the Bun child (and its
35
+ // own descendants) directly; `team-orchestrator.ts` does exactly that.
6
36
 
7
- const { execFileSync, execSync } = require("node:child_process");
37
+ const { execFileSync, execSync, spawn } = require("node:child_process");
8
38
  const { resolve } = require("node:path");
9
39
 
10
40
  /**
@@ -75,18 +105,64 @@ Learn more: https://bun.sh`);
75
105
  process.exit(1);
76
106
  }
77
107
 
78
- // Exec into bun with the real entry point
79
108
  const entry = resolve(__dirname, "..", "dist", "index.js");
109
+
110
+ let child;
80
111
  try {
81
- const result = require("node:child_process").spawnSync(bun, [entry, ...process.argv.slice(2)], {
112
+ child = spawn(bun, [entry, ...process.argv.slice(2)], {
82
113
  stdio: "inherit",
83
114
  env: process.env,
84
115
  });
85
- process.exit(result.status ?? 1);
86
116
  } catch (err) {
87
117
  console.error("Failed to start claudish:", err.message);
88
118
  process.exit(1);
89
119
  }
120
+
121
+ // Forward the signals a supervisor, terminal, or parent process actually sends.
122
+ // SIGKILL is deliberately absent — it cannot be caught, which is the whole
123
+ // reason callers wanting a hard kill must target the process group instead.
124
+ //
125
+ // Handlers are installed only AFTER a successful spawn, so a signal arriving
126
+ // during startup keeps Node's default terminate-immediately behaviour rather
127
+ // than being swallowed by a forward to a child that does not exist yet.
128
+ const FORWARDED =
129
+ process.platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
130
+ for (const signal of FORWARDED) {
131
+ process.on(signal, () => {
132
+ // Best-effort: the child may have exited between the signal and this line.
133
+ try {
134
+ if (!child.killed) child.kill(signal);
135
+ } catch {}
136
+ // Do NOT exit here. Waiting for the child's own "exit" keeps our status
137
+ // honest and, more importantly, keeps this process alive as long as the
138
+ // child is — a caller watching OUR pid must not see us disappear while the
139
+ // real work is still running.
140
+ });
141
+ }
142
+
143
+ child.on("error", (err) => {
144
+ console.error("Failed to start claudish:", err.message);
145
+ process.exit(1);
146
+ });
147
+
148
+ child.on("exit", (code, signal) => {
149
+ if (signal) {
150
+ // Re-raise the signal on ourselves so our parent observes the same cause
151
+ // of death the child had, rather than a synthesised exit code. The handler
152
+ // is removed first, or we would forward it straight back to a dead child.
153
+ process.removeAllListeners(signal);
154
+ try {
155
+ process.kill(process.pid, signal);
156
+ return;
157
+ } catch {
158
+ // Re-raise failed (unsupported signal on this platform) — fall back to
159
+ // the shell convention for "died from signal N".
160
+ const NUMBERS = { SIGINT: 2, SIGQUIT: 3, SIGKILL: 9, SIGTERM: 15, SIGHUP: 1 };
161
+ process.exit(128 + (NUMBERS[signal] ?? 0));
162
+ }
163
+ }
164
+ process.exit(code ?? 1);
165
+ });
90
166
  }
91
167
 
92
168
  // Running it as a program launches. Requiring it exposes the helpers so the
package/dist/index.js CHANGED
@@ -729,7 +729,7 @@ var init_onepassword_config = __esm(() => {
729
729
  });
730
730
 
731
731
  // src/version.ts
732
- var VERSION = "7.51.0";
732
+ var VERSION = "7.53.0";
733
733
 
734
734
  // src/logger.ts
735
735
  var exports_logger = {};
@@ -30167,6 +30167,9 @@ function parseRecord(raw) {
30167
30167
  return null;
30168
30168
  }
30169
30169
  }
30170
+ function encodeRecord(rec) {
30171
+ return PREFIX + Buffer.from(JSON.stringify(rec), "utf8").toString("base64");
30172
+ }
30170
30173
  function readSharedAntigravityToken(deps = defaultDeps) {
30171
30174
  const rec = parseRecord(deps.readStore());
30172
30175
  return rec ? rec.token : null;
@@ -30184,6 +30187,15 @@ function hasSharedAntigravityToken(deps = defaultDeps) {
30184
30187
  cachedHasToken = { at: now, value };
30185
30188
  return value;
30186
30189
  }
30190
+ function writeSharedAntigravityToken(tok, deps = defaultDeps) {
30191
+ const existing = parseRecord(deps.readStore());
30192
+ const base = existing ?? { token: tok };
30193
+ const merged = {
30194
+ ...base,
30195
+ token: { ...base.token, ...tok }
30196
+ };
30197
+ deps.writeStore(encodeRecord(merged));
30198
+ }
30187
30199
  function deleteSharedAntigravityToken(deps = defaultDeps) {
30188
30200
  (deps.deleteStore ?? defaultDeleteStore)();
30189
30201
  _resetAntigravityTokenState();
@@ -30222,6 +30234,31 @@ function getValidAntigravityAccessToken(deps = defaultDeps) {
30222
30234
  });
30223
30235
  return inFlight;
30224
30236
  }
30237
+ async function forceRefreshAntigravityToken(deps = defaultDeps) {
30238
+ if (process.platform !== "darwin") {
30239
+ throw new Error("[Antigravity] The shared Antigravity token store is macOS-only for now. " + "Use g@<model> with GEMINI_API_KEY on this platform.");
30240
+ }
30241
+ _resetAntigravityTokenState();
30242
+ const rec = parseRecord(deps.readStore());
30243
+ if (!rec) {
30244
+ throw new Error("[Antigravity] No Antigravity session found. Sign in with `claudish login antigravity`, " + "or use g@<model> with GEMINI_API_KEY " + "(get one at https://aistudio.google.com/app/apikey).");
30245
+ }
30246
+ const original = rec.token;
30247
+ log("[Antigravity] Upstream rejected the current token \u2014 asking the Antigravity CLI to re-mint.");
30248
+ writeSharedAntigravityToken({ ...original, expiry: new Date(deps.now() - 1000).toISOString() }, deps);
30249
+ deps.runAgyRefresh();
30250
+ const refreshed = parseRecord(deps.readStore());
30251
+ const token = refreshed?.token;
30252
+ if (token && token.access_token !== original.access_token && !needsRefresh(token, deps.now())) {
30253
+ log("[Antigravity] Shared token re-minted by the Antigravity CLI.");
30254
+ return token.access_token;
30255
+ }
30256
+ if (!refreshed || refreshed.token.access_token === original.access_token) {
30257
+ writeSharedAntigravityToken(original, deps);
30258
+ }
30259
+ _resetAntigravityTokenState();
30260
+ throw new Error("[Antigravity] The Antigravity session was rejected upstream and could not be re-minted. " + "Run `claudish login antigravity` to sign in again.");
30261
+ }
30225
30262
  function _resetAntigravityTokenState() {
30226
30263
  inFlight = null;
30227
30264
  cachedHasToken = null;
@@ -30248,6 +30285,13 @@ function makeTerminalSetupError(message) {
30248
30285
  function buildAntigravityUserAgent() {
30249
30286
  return `antigravity/cli/1.1.9 (aidev_client; os_type=${process.platform}; arch=${process.arch}; auth_method=consumer)`;
30250
30287
  }
30288
+ function resetAntigravityUserCache() {
30289
+ cachedAgProjectId = null;
30290
+ cachedAgTierId = null;
30291
+ cachedAgTierName = null;
30292
+ agServedCache = null;
30293
+ agServedCacheAt = 0;
30294
+ }
30251
30295
  async function callLoadCodeAssistAntigravity(accessToken) {
30252
30296
  const res = await fetch(`${ANTIGRAVITY_API_BASE}:loadCodeAssist`, {
30253
30297
  method: "POST",
@@ -30632,6 +30676,22 @@ class AntigravityProviderTransport {
30632
30676
  this.servedModelName = resolveAntigravityModelId(this.modelName, this.servedModels, this.defaultServedModel, lookupFamilyDefaultVariant(this.modelName, "antigravity"));
30633
30677
  log(`[Antigravity] Auth refreshed, project: ${this.projectId}, tier: ${this._displayName}, ` + `model: ${this.modelName} -> ${this.servedModelName}, served: ${this.servedModels.join(",") || "(none)"}`);
30634
30678
  }
30679
+ async forceRefreshAuth() {
30680
+ await forceRefreshAntigravityToken();
30681
+ resetAntigravityUserCache();
30682
+ this.cachedAuth = null;
30683
+ await this.refreshAuth();
30684
+ }
30685
+ classifyTerminalError(status, bodyText) {
30686
+ if (status !== 429)
30687
+ return;
30688
+ const classification = classify429(bodyText);
30689
+ if (!classification)
30690
+ return;
30691
+ if (classification.reason === "MODEL_CAPACITY_EXHAUSTED")
30692
+ return false;
30693
+ return classification.terminal;
30694
+ }
30635
30695
  transformPayload(payload) {
30636
30696
  const envelope = this.cachedAuth?.transformPayload ? this.cachedAuth.transformPayload(payload) : this.buildEnvelope(payload, this.servedModelName);
30637
30697
  this.lastEnvelope = envelope;
@@ -30672,8 +30732,7 @@ class AntigravityProviderTransport {
30672
30732
  return this.handleCapacityExhausted(response, queue);
30673
30733
  }
30674
30734
  if (classification.terminal) {
30675
- logStderr(`[Antigravity] Quota exhausted (${classification.reason || "daily limit"}). Check plan limits.`);
30676
- return response;
30735
+ return await this.explainTerminalQuota(response, bodyText, classification.reason);
30677
30736
  }
30678
30737
  if (attempt < MAX_RETRY_ATTEMPTS) {
30679
30738
  const delay = classification.retryDelayMs ?? DEFAULT_RATE_LIMIT_DELAY_MS;
@@ -30768,6 +30827,53 @@ class AntigravityProviderTransport {
30768
30827
  headers: { "Content-Type": "application/json" }
30769
30828
  });
30770
30829
  }
30830
+ async quotaRemainingForServedModel() {
30831
+ if (!this.accessToken || !this.projectId)
30832
+ return;
30833
+ let timer;
30834
+ const data = await Promise.race([
30835
+ retrieveUserQuota(this.accessToken, this.projectId).catch(() => null),
30836
+ new Promise((resolve) => {
30837
+ timer = setTimeout(() => resolve(null), QUOTA_CHECK_TIMEOUT_MS);
30838
+ })
30839
+ ]).finally(() => {
30840
+ if (timer)
30841
+ clearTimeout(timer);
30842
+ });
30843
+ const buckets = data?.buckets;
30844
+ if (!buckets?.length)
30845
+ return;
30846
+ const bucket = buckets.find((b) => b.modelId === this.servedModelName) ?? buckets.find((b) => b.modelId === this.modelName);
30847
+ return typeof bucket?.remainingFraction === "number" ? bucket.remainingFraction : undefined;
30848
+ }
30849
+ async explainTerminalQuota(response, bodyText, reason) {
30850
+ const remaining = await this.quotaRemainingForServedModel();
30851
+ const model = this.servedModelName;
30852
+ let advice;
30853
+ if (remaining !== undefined && remaining > 0) {
30854
+ advice = `your Antigravity plan still reports ${(remaining * 100).toFixed(1)}% of the ${model} ` + "allowance available, so this is probably NOT an exhausted plan. The usual cause is an " + "Antigravity session Google invalidated server-side; run `claudish login antigravity`.";
30855
+ } else if (remaining === undefined) {
30856
+ advice = "claudish could not read your Antigravity quota to confirm this. If your plan is not " + "actually spent, the session may be stale \u2014 run `claudish login antigravity`.";
30857
+ } else {
30858
+ advice = `your Antigravity plan reports no remaining ${model} allowance; it refills on Google's own schedule.`;
30859
+ }
30860
+ logStderr(`[Antigravity] Terminal 429 (${reason || "daily limit"}) \u2014 ${advice}`);
30861
+ let parsed;
30862
+ try {
30863
+ parsed = JSON.parse(bodyText);
30864
+ } catch {
30865
+ return response;
30866
+ }
30867
+ const target = Array.isArray(parsed) ? parsed[0]?.error : parsed?.error;
30868
+ if (!target || typeof target !== "object")
30869
+ return response;
30870
+ const upstream = typeof target.message === "string" && target.message.length > 0 ? target.message : "Resource has been exhausted";
30871
+ target.message = `${upstream} \u2014 ${advice}`;
30872
+ return new Response(JSON.stringify(parsed), {
30873
+ status: 429,
30874
+ headers: { "Content-Type": "application/json" }
30875
+ });
30876
+ }
30771
30877
  async logQuotaInfo() {
30772
30878
  if (!this.accessToken || !this.projectId)
30773
30879
  return;
@@ -30776,6 +30882,7 @@ class AntigravityProviderTransport {
30776
30882
  if (!data?.buckets?.length)
30777
30883
  return;
30778
30884
  const lines = [];
30885
+ let sawThrottledModel = false;
30779
30886
  for (const bucket of data.buckets) {
30780
30887
  if (!bucket.modelId)
30781
30888
  continue;
@@ -30784,15 +30891,24 @@ class AntigravityProviderTransport {
30784
30891
  hour: "2-digit",
30785
30892
  minute: "2-digit"
30786
30893
  }) : "?";
30787
- lines.push(` ${bucket.modelId}: ${pct} remaining (resets ${reset})`);
30894
+ const mine = this.isThrottledModel(bucket.modelId);
30895
+ if (mine)
30896
+ sawThrottledModel = true;
30897
+ lines.push(` ${mine ? "> " : " "}${bucket.modelId}: ${pct} remaining (resets ${reset})`);
30788
30898
  }
30789
- if (lines.length > 0) {
30790
- logStderr(`[Antigravity] Quota status:
30899
+ if (lines.length === 0)
30900
+ return;
30901
+ const requested = this.servedModelName && this.servedModelName !== this.modelName ? `${this.modelName} (served as ${this.servedModelName})` : this.modelName;
30902
+ const header = sawThrottledModel ? `[Antigravity] Quota status (> = ${requested}):` : `[Antigravity] Quota status \u2014 NOTE: ${requested} has no quota bucket below, ` + "so these figures do not explain its rate limit:";
30903
+ logStderr(`${header}
30791
30904
  ${lines.join(`
30792
30905
  `)}`);
30793
- }
30794
30906
  } catch {}
30795
30907
  }
30908
+ isThrottledModel(bucketModelId) {
30909
+ const id = bucketModelId.toLowerCase();
30910
+ return id === this.modelName.toLowerCase() || id === this.servedModelName.toLowerCase();
30911
+ }
30796
30912
  async getQuotaRemaining(modelName) {
30797
30913
  if (!this.accessToken || !this.projectId)
30798
30914
  return;
@@ -30807,7 +30923,7 @@ ${lines.join(`
30807
30923
  }
30808
30924
  }
30809
30925
  }
30810
- var ANTIGRAVITY_BASE = "https://cloudcode-pa.googleapis.com", ANTIGRAVITY_ENDPOINT, MAX_RETRY_ATTEMPTS = 3, DEFAULT_RATE_LIMIT_DELAY_MS = 1e4, REASONING_TIER_RANK;
30926
+ var ANTIGRAVITY_BASE = "https://cloudcode-pa.googleapis.com", ANTIGRAVITY_ENDPOINT, MAX_RETRY_ATTEMPTS = 3, DEFAULT_RATE_LIMIT_DELAY_MS = 1e4, QUOTA_CHECK_TIMEOUT_MS = 3000, REASONING_TIER_RANK;
30811
30927
  var init_antigravity = __esm(() => {
30812
30928
  init_model_catalog();
30813
30929
  init_antigravity_token();
@@ -32675,12 +32791,14 @@ var init_deepseek_model_dialect = __esm(() => {
32675
32791
  }
32676
32792
  applyNativeReasoning(request, originalRequest) {
32677
32793
  const effort = this.resolveEffortLevel(originalRequest);
32678
- if (effort && this.isV4Model()) {
32794
+ if (effort && this.acceptsReasoningControls()) {
32679
32795
  if (effort === "none" || effort === "minimal") {
32680
32796
  request.thinking = { type: "disabled" };
32681
32797
  log(`[DeepSeekModelDialect] effort ${effort} -> thinking.type: disabled for ${this.modelId}`);
32682
32798
  } else {
32683
- const value = effort === "xhigh" || effort === "max" ? "max" : "high";
32799
+ const reasoning = this.lookupReasoningCapability();
32800
+ const clamped = reasoning?.control === "effort" && reasoning.efforts?.length ? this.clampToAdvertisedEffort(effort, reasoning) : undefined;
32801
+ const value = clamped ?? (effort === "xhigh" || effort === "max" ? "max" : "high");
32684
32802
  request.reasoning_effort = value;
32685
32803
  log(`[DeepSeekModelDialect] effort ${effort} -> reasoning_effort: ${value} for ${this.modelId}`);
32686
32804
  }
@@ -32692,9 +32810,15 @@ var init_deepseek_model_dialect = __esm(() => {
32692
32810
  }
32693
32811
  return request;
32694
32812
  }
32695
- isV4Model() {
32813
+ acceptsReasoningControls() {
32814
+ const reasoning = this.lookupReasoningCapability();
32815
+ if (reasoning)
32816
+ return reasoning.supported === true && reasoning.control === "effort";
32696
32817
  const model = this.modelId.toLowerCase();
32697
- return model.includes("v4") || model.includes("deepseek-chat") || model.includes("deepseek-reasoner");
32818
+ if (model.includes("deepseek-chat") || model.includes("deepseek-reasoner"))
32819
+ return true;
32820
+ const version2 = /(?:^|[^a-z0-9])v(\d+)/.exec(model);
32821
+ return version2 ? Number(version2[1]) >= 4 : false;
32698
32822
  }
32699
32823
  shouldHandle(modelId) {
32700
32824
  return matchesModelFamily(modelId, "deepseek");
@@ -34105,11 +34229,87 @@ var init_glm_model_dialect = __esm(() => {
34105
34229
  };
34106
34230
  });
34107
34231
 
34232
+ // src/adapters/grok-effort-support.ts
34233
+ function acceptsReasoningEffort(modelId) {
34234
+ const id = norm(modelId);
34235
+ if (!id)
34236
+ return false;
34237
+ if (learnedRejects.has(id))
34238
+ return false;
34239
+ const reasoning = lookupModelReasoning(id) ?? lookupModelReasoning(modelId);
34240
+ if (reasoning)
34241
+ return reasoning.supported === true && reasoning.control === "effort";
34242
+ return !SEED_REJECTS.some((re) => re.test(id));
34243
+ }
34244
+ function catalogReasoningFor(modelId) {
34245
+ return lookupModelReasoning(norm(modelId)) ?? lookupModelReasoning(modelId);
34246
+ }
34247
+ function rememberReasoningEffortRejected(modelId) {
34248
+ const id = norm(modelId);
34249
+ if (!id || learnedRejects.has(id))
34250
+ return;
34251
+ learnedRejects.add(id);
34252
+ log(`[GrokEffortSupport] ${id} rejected reasoning_effort \u2014 not sending it again this session.`);
34253
+ }
34254
+ function isReasoningEffortRejection(errorText) {
34255
+ if (!errorText)
34256
+ return false;
34257
+ return /does not support parameter\s+`?reasoning[_]?effort`?/i.test(errorText);
34258
+ }
34259
+ function acceptsReasoningEffortValue(modelId, value) {
34260
+ return !learnedValueRejects.has(pairKey(modelId, value));
34261
+ }
34262
+ function rememberReasoningEffortValueRejected(modelId, value) {
34263
+ const key = pairKey(modelId, value);
34264
+ if (learnedValueRejects.has(key))
34265
+ return;
34266
+ learnedValueRejects.add(key);
34267
+ log(`[GrokEffortSupport] ${norm(modelId)} rejected reasoning_effort value "${value}" \u2014 ` + "falling back and not sending it again this session.");
34268
+ }
34269
+ function rejectedReasoningEffortValue(errorText) {
34270
+ if (!errorText)
34271
+ return null;
34272
+ const m = /does not support\s+`?reasoning[_]?effort`?\s+value\s+`?([a-z]+)`?/i.exec(errorText);
34273
+ return m ? m[1].toLowerCase() : null;
34274
+ }
34275
+ function fallbackReasoningEffortValue(value) {
34276
+ switch (value.toLowerCase()) {
34277
+ case "none":
34278
+ return "low";
34279
+ case "minimal":
34280
+ return "low";
34281
+ case "low":
34282
+ return null;
34283
+ case "medium":
34284
+ return "low";
34285
+ case "high":
34286
+ return "medium";
34287
+ default:
34288
+ return null;
34289
+ }
34290
+ }
34291
+ var SEED_REJECTS, learnedRejects, norm = (modelId) => modelId.trim().toLowerCase().replace(/^x-ai\//, ""), learnedValueRejects, pairKey = (modelId, value) => `${norm(modelId)}\x00${value.toLowerCase()}`;
34292
+ var init_grok_effort_support = __esm(() => {
34293
+ init_logger();
34294
+ init_model_catalog();
34295
+ SEED_REJECTS = [
34296
+ /non-reasoning/i,
34297
+ /^grok-2/i,
34298
+ /^grok-4(?![.\d])(?!.*fast-reasoning)/i,
34299
+ /^grok-build-/i,
34300
+ /^grok-code-/i,
34301
+ /^grok-4\.20/i
34302
+ ];
34303
+ learnedRejects = new Set;
34304
+ learnedValueRejects = new Set;
34305
+ });
34306
+
34108
34307
  // src/adapters/grok-model-dialect.ts
34109
34308
  var GrokModelDialect;
34110
34309
  var init_grok_model_dialect = __esm(() => {
34111
34310
  init_logger();
34112
34311
  init_base_api_format();
34312
+ init_grok_effort_support();
34113
34313
  init_model_catalog();
34114
34314
  GrokModelDialect = class GrokModelDialect extends BaseAPIFormat {
34115
34315
  xmlBuffer = "";
@@ -34172,34 +34372,78 @@ var init_grok_model_dialect = __esm(() => {
34172
34372
  return request;
34173
34373
  }
34174
34374
  effortToReasoningEffort(effort) {
34175
- const model = this.modelId.toLowerCase();
34176
- const isMini = model.includes("mini");
34177
- const isGrok43 = /grok-4\.3(\b|[-.]|$)/.test(model);
34178
- const isFastReasoning = model.includes("fast-reasoning");
34179
- if (!(isMini || isGrok43 || isFastReasoning)) {
34375
+ if (!acceptsReasoningEffort(this.modelId)) {
34180
34376
  return;
34181
34377
  }
34182
- if (isMini) {
34183
- switch (effort) {
34184
- case "high":
34185
- case "xhigh":
34186
- case "max":
34187
- return "high";
34188
- default:
34189
- return "low";
34378
+ let value;
34379
+ const reasoning = catalogReasoningFor(this.modelId);
34380
+ const clamped = reasoning ? this.clampToAdvertisedEffort(effort, reasoning) : undefined;
34381
+ if (clamped) {
34382
+ value = clamped;
34383
+ } else {
34384
+ const model = this.modelId.toLowerCase();
34385
+ const isMini = model.includes("mini");
34386
+ if (isMini) {
34387
+ switch (effort) {
34388
+ case "high":
34389
+ case "xhigh":
34390
+ case "max":
34391
+ value = "high";
34392
+ break;
34393
+ default:
34394
+ value = "low";
34395
+ }
34396
+ } else {
34397
+ switch (effort) {
34398
+ case "none":
34399
+ value = "none";
34400
+ break;
34401
+ case "minimal":
34402
+ case "low":
34403
+ value = "low";
34404
+ break;
34405
+ case "medium":
34406
+ value = "medium";
34407
+ break;
34408
+ default:
34409
+ value = "high";
34410
+ }
34190
34411
  }
34191
34412
  }
34192
- switch (effort) {
34193
- case "none":
34194
- return "none";
34195
- case "minimal":
34196
- case "low":
34197
- return "low";
34198
- case "medium":
34199
- return "medium";
34200
- default:
34201
- return "high";
34413
+ let guard = 0;
34414
+ while (!acceptsReasoningEffortValue(this.modelId, value) && guard++ < 8) {
34415
+ const next = fallbackReasoningEffortValue(value);
34416
+ if (!next)
34417
+ return;
34418
+ value = next;
34419
+ }
34420
+ return value;
34421
+ }
34422
+ recoverFromRejection(payload, errorText) {
34423
+ if (!payload || payload.reasoning_effort === undefined)
34424
+ return null;
34425
+ if (isReasoningEffortRejection(errorText)) {
34426
+ rememberReasoningEffortRejected(this.modelId);
34427
+ const next = { ...payload };
34428
+ delete next.reasoning_effort;
34429
+ return { payload: next, note: `dropped reasoning_effort for ${this.modelId}` };
34430
+ }
34431
+ const rejected = rejectedReasoningEffortValue(errorText);
34432
+ if (rejected) {
34433
+ rememberReasoningEffortValueRejected(this.modelId, rejected);
34434
+ const fallback = fallbackReasoningEffortValue(rejected);
34435
+ const next = { ...payload };
34436
+ if (fallback) {
34437
+ next.reasoning_effort = fallback;
34438
+ return {
34439
+ payload: next,
34440
+ note: `reasoning_effort "${rejected}" -> "${fallback}" for ${this.modelId}`
34441
+ };
34442
+ }
34443
+ delete next.reasoning_effort;
34444
+ return { payload: next, note: `dropped unsupported reasoning_effort for ${this.modelId}` };
34202
34445
  }
34446
+ return null;
34203
34447
  }
34204
34448
  parseXmlParameters(xmlContent) {
34205
34449
  const params = {};
@@ -36079,6 +36323,45 @@ Do not invent a different filename, and do not derive one from the task. Claude
36079
36323
  PLAN_MODE_RULES = [planFilePathRule];
36080
36324
  });
36081
36325
 
36326
+ // src/behavior/rules/session-context.ts
36327
+ function hasInjectedSessionContext(systemText2, messages) {
36328
+ const haystacks = [systemText2 ?? ""];
36329
+ const first = Array.isArray(messages) ? messages[0] : undefined;
36330
+ if (first && typeof first === "object") {
36331
+ const content = first.content;
36332
+ if (typeof content === "string") {
36333
+ haystacks.push(content);
36334
+ } else if (Array.isArray(content)) {
36335
+ for (const block of content) {
36336
+ const text = block?.text;
36337
+ if (typeof text === "string")
36338
+ haystacks.push(text);
36339
+ }
36340
+ }
36341
+ }
36342
+ return haystacks.some((h) => {
36343
+ const lower = h.toLowerCase();
36344
+ return SESSION_CONTEXT_MARKERS.some((m) => lower.includes(m));
36345
+ });
36346
+ }
36347
+ var SESSION_CONTEXT_MARKERS, NOTE, noHarnessEchoRule, SESSION_CONTEXT_RULES;
36348
+ var init_session_context = __esm(() => {
36349
+ SESSION_CONTEXT_MARKERS = ["sessionstart hook additional context", "hook additional context"];
36350
+ NOTE = "Operational context note. This session may include text injected by the harness " + "itself \u2014 SessionStart hook output, coaching or insight blocks, tooling banners, " + "status lines. That material is addressed to you, not to the user, and it is not " + "part of the task. Never reproduce, summarise, or quote it in your answer, and never " + "let it open your response. Begin your answer with the requested deliverable and " + "nothing before it.";
36351
+ noHarnessEchoRule = {
36352
+ id: "session-context/no-harness-echo",
36353
+ description: "Stop foreign models opening their answer with harness-injected session context " + "(SessionStart hook output, coaching/insight blocks).",
36354
+ defaultSeverity: "fix",
36355
+ appliesTo: ({ isNativeAnthropic }) => !isNativeAnthropic,
36356
+ onRequest(ctx) {
36357
+ if (!hasInjectedSessionContext(ctx.systemText, ctx.messages))
36358
+ return [];
36359
+ return [{ type: "injectSystemNote", text: NOTE }];
36360
+ }
36361
+ };
36362
+ SESSION_CONTEXT_RULES = [noHarnessEchoRule];
36363
+ });
36364
+
36082
36365
  // src/behavior/hooks.ts
36083
36366
  import { isAbsolute, resolve } from "path";
36084
36367
  function isBehaviorRule(value) {
@@ -36288,6 +36571,7 @@ var init_behavior = __esm(() => {
36288
36571
  init_config();
36289
36572
  init_engine();
36290
36573
  init_plan_mode();
36574
+ init_session_context();
36291
36575
  init_engine();
36292
36576
  init_config();
36293
36577
  init_harness();
@@ -36297,7 +36581,7 @@ var init_behavior = __esm(() => {
36297
36581
  init_corpus();
36298
36582
  init_aggregate();
36299
36583
  init_upload();
36300
- BUILTIN_RULES = [...PLAN_MODE_RULES];
36584
+ BUILTIN_RULES = [...PLAN_MODE_RULES, ...SESSION_CONTEXT_RULES];
36301
36585
  hookRules = [];
36302
36586
  });
36303
36587
 
@@ -40840,6 +41124,30 @@ class ComposedHandler {
40840
41124
  }
40841
41125
  log(`[${this.provider.displayName}] Response status: ${response.status}`);
40842
41126
  this.capturePlanUsage(response);
41127
+ if (!response.ok) {
41128
+ if (response.status >= 400 && response.status < 500 && this.modelAdapter?.recoverFromRejection) {
41129
+ const errorText = await response.clone().text();
41130
+ const recovery = this.modelAdapter.recoverFromRejection(requestPayload, errorText);
41131
+ if (recovery) {
41132
+ log(`[${this.provider.displayName}] Parameter rejected \u2014 retrying: ${recovery.note}`);
41133
+ requestPayload = recovery.payload;
41134
+ const retrySerialized = this.provider.serializeBody?.(requestPayload);
41135
+ const retryHeaders = await this.provider.getHeaders();
41136
+ retryHeaders["Content-Type"] = retrySerialized?.contentType ?? "application/json";
41137
+ const retryResp = await fetch(endpoint, {
41138
+ method: "POST",
41139
+ headers: retryHeaders,
41140
+ body: retrySerialized?.body ?? JSON.stringify(requestPayload),
41141
+ ...this.provider.getRequestInit?.() || {}
41142
+ });
41143
+ if (retryResp.ok) {
41144
+ response = retryResp;
41145
+ } else {
41146
+ log(`[${this.provider.displayName}] Retry after ${recovery.note} still failed ` + `(HTTP ${retryResp.status})`);
41147
+ }
41148
+ }
41149
+ }
41150
+ }
40843
41151
  if (!response.ok) {
40844
41152
  if (response.status === 401 && this.provider.forceRefreshAuth) {
40845
41153
  log(`[${this.provider.displayName}] Got 401, forcing auth refresh and retrying`);
@@ -40934,7 +41242,8 @@ class ComposedHandler {
40934
41242
  } else {
40935
41243
  const errorText = await response.text();
40936
41244
  log(`[${this.provider.displayName}] Error: ${errorText}`);
40937
- const hint = getRecoveryHint(response.status, errorText, this.provider.displayName);
41245
+ const transportTerminal = this.provider.classifyTerminalError?.(response.status, errorText);
41246
+ const hint = getRecoveryHint(response.status, errorText, this.provider.displayName, transportTerminal);
40938
41247
  let parsedErrorBody;
40939
41248
  try {
40940
41249
  parsedErrorBody = JSON.parse(errorText);
@@ -40987,7 +41296,7 @@ class ComposedHandler {
40987
41296
  const errorBody = parsedErrorBody ?? {
40988
41297
  error: { type: "api_error", message: errorText }
40989
41298
  };
40990
- if (isTerminalError(response.status, errorText, isTerminal429(errorText))) {
41299
+ if (isTerminalError(response.status, errorText, transportTerminal ?? isTerminal429(errorText))) {
40991
41300
  const surfaced = buildSurfacedErrorMessage({
40992
41301
  providerDisplayName: this.provider.displayName,
40993
41302
  status: response.status,
@@ -41363,14 +41672,17 @@ class ComposedHandler {
41363
41672
  }
41364
41673
  }
41365
41674
  }
41366
- function getRecoveryHint(status, errorText, providerName) {
41675
+ function getRecoveryHint(status, errorText, providerName, transportTerminal429) {
41367
41676
  const lower = errorText.toLowerCase();
41368
41677
  if (status === 503 || lower.includes("overloaded")) {
41369
41678
  return "Provider overloaded. Retry or use a different model.";
41370
41679
  }
41371
- if (status === 429 && isTerminal429(errorText)) {
41680
+ if (status === 429 && (transportTerminal429 ?? isTerminal429(errorText))) {
41372
41681
  return "Out of quota \u2014 check your plan & billing details. This won't recover on retry.";
41373
41682
  }
41683
+ if (status === 429 && transportTerminal429 === false) {
41684
+ return "Rate limited. Wait, reduce concurrency, or check plan limits.";
41685
+ }
41374
41686
  if (isQuotaExhaustionError(status, errorText)) {
41375
41687
  return "Subscription allowance spent \u2014 this refills on the provider's own schedule (see the message below). Reducing concurrency won't help; switch model/provider or wait.";
41376
41688
  }
@@ -44643,6 +44955,48 @@ var init_signal_watcher = __esm(() => {
44643
44955
  QUESTION_PATTERNS = [/\?\s*$/m, /\bchoose\b.*:/im, /\bselect\b.*:/im, /\benter\b.*:/im];
44644
44956
  });
44645
44957
 
44958
+ // src/process-tree.ts
44959
+ function signalProcessTree(proc, signal) {
44960
+ if (KILL_PROCESS_GROUP && proc.pid) {
44961
+ try {
44962
+ process.kill(-proc.pid, signal);
44963
+ } catch {}
44964
+ }
44965
+ try {
44966
+ if (!proc.killed)
44967
+ proc.kill(signal);
44968
+ } catch {}
44969
+ }
44970
+ function waitForExit(proc, ms) {
44971
+ if (proc.exitCode !== null || proc.signalCode !== null)
44972
+ return Promise.resolve(true);
44973
+ return new Promise((resolve2) => {
44974
+ let settled = false;
44975
+ const done = (value) => {
44976
+ if (settled)
44977
+ return;
44978
+ settled = true;
44979
+ proc.off("exit", onExit);
44980
+ resolve2(value);
44981
+ };
44982
+ const onExit = () => done(true);
44983
+ proc.once("exit", onExit);
44984
+ const t = setTimeout(() => done(false), ms);
44985
+ t.unref?.();
44986
+ });
44987
+ }
44988
+ async function terminateChildTree(proc, graceMs = TERMINATE_GRACE_MS) {
44989
+ signalProcessTree(proc, "SIGTERM");
44990
+ if (await waitForExit(proc, graceMs))
44991
+ return true;
44992
+ signalProcessTree(proc, "SIGKILL");
44993
+ return waitForExit(proc, graceMs);
44994
+ }
44995
+ var KILL_PROCESS_GROUP, TERMINATE_GRACE_MS = 5000;
44996
+ var init_process_tree = __esm(() => {
44997
+ KILL_PROCESS_GROUP = process.platform !== "win32";
44998
+ });
44999
+
44646
45000
  // src/spawn-claudish.ts
44647
45001
  function resolveClaudishSpawn(env = process.env) {
44648
45002
  const bin = env[CLAUDISH_BIN_ENV]?.trim();
@@ -44699,7 +45053,8 @@ class SessionManager {
44699
45053
  const proc = spawn(spawnTarget.command, [...spawnTarget.prefixArgs, ...args], {
44700
45054
  cwd: opts.cwd ?? process.cwd(),
44701
45055
  stdio: ["pipe", "pipe", "pipe"],
44702
- shell: false
45056
+ shell: false,
45057
+ detached: KILL_PROCESS_GROUP
44703
45058
  });
44704
45059
  const scrollback = new ScrollbackBuffer(this.scrollbackCapacity);
44705
45060
  const watcher = new SignalWatcher(sessionId2, (sid, data) => {
@@ -44779,10 +45134,10 @@ class SessionManager {
44779
45134
  });
44780
45135
  entry.timeoutHandle = setTimeout(() => {
44781
45136
  if (!proc.killed) {
44782
- proc.kill("SIGTERM");
45137
+ signalProcessTree(proc, "SIGTERM");
44783
45138
  entry.killHandle = setTimeout(() => {
44784
45139
  try {
44785
- proc.kill("SIGKILL");
45140
+ signalProcessTree(proc, "SIGKILL");
44786
45141
  } catch {}
44787
45142
  }, KILL_GRACE_MS);
44788
45143
  entry.info.status = "timeout";
@@ -44840,10 +45195,10 @@ class SessionManager {
44840
45195
  entry.info.completedAt = new Date().toISOString();
44841
45196
  entry.watcher.forceState("cancelled", "Session cancelled");
44842
45197
  if (!entry.process.killed) {
44843
- entry.process.kill("SIGTERM");
45198
+ signalProcessTree(entry.process, "SIGTERM");
44844
45199
  entry.killHandle = setTimeout(() => {
44845
45200
  try {
44846
- entry.process.kill("SIGKILL");
45201
+ signalProcessTree(entry.process, "SIGKILL");
44847
45202
  } catch {}
44848
45203
  }, KILL_GRACE_MS);
44849
45204
  }
@@ -44874,11 +45229,11 @@ class SessionManager {
44874
45229
  const promises = [];
44875
45230
  for (const [, entry] of this.sessions) {
44876
45231
  if (!entry.process.killed) {
44877
- entry.process.kill("SIGTERM");
45232
+ signalProcessTree(entry.process, "SIGTERM");
44878
45233
  promises.push(new Promise((resolve2) => {
44879
45234
  const timeout = setTimeout(() => {
44880
45235
  try {
44881
- entry.process.kill("SIGKILL");
45236
+ signalProcessTree(entry.process, "SIGKILL");
44882
45237
  } catch {}
44883
45238
  resolve2();
44884
45239
  }, KILL_GRACE_MS);
@@ -44924,6 +45279,7 @@ class SessionManager {
44924
45279
  }
44925
45280
  var DEFAULT_MAX_SESSIONS = 20, DEFAULT_SCROLLBACK = 2000, DEFAULT_TIMEOUT = 600, MAX_TIMEOUT = 3600, KILL_GRACE_MS = 5000;
44926
45281
  var init_session_manager = __esm(() => {
45282
+ init_process_tree();
44927
45283
  init_scrollback_buffer();
44928
45284
  init_signal_watcher();
44929
45285
  });
@@ -50810,6 +51166,7 @@ __export(exports_team_orchestrator, {
50810
51166
  runModels: () => runModels,
50811
51167
  resolveCaptureMode: () => resolveCaptureMode,
50812
51168
  parseJudgeVotes: () => parseJudgeVotes,
51169
+ meaningfulStderr: () => meaningfulStderr,
50813
51170
  judgeResponses: () => judgeResponses,
50814
51171
  getStatus: () => getStatus,
50815
51172
  fisherYatesShuffle: () => fisherYatesShuffle,
@@ -50818,6 +51175,9 @@ __export(exports_team_orchestrator, {
50818
51175
  aggregateVerdict: () => aggregateVerdict,
50819
51176
  TEAM_CAPTURE_ENV_VAR: () => TEAM_CAPTURE_ENV_VAR,
50820
51177
  STDOUT_TAIL_LIMIT: () => STDOUT_TAIL_LIMIT,
51178
+ GRACE_INTERVAL_MS: () => GRACE_INTERVAL_MS,
51179
+ DRAIN_TIMEOUT_MS: () => DRAIN_TIMEOUT_MS,
51180
+ DEFAULT_STALL_SECONDS: () => DEFAULT_STALL_SECONDS,
50821
51181
  DEFAULT_MIN_OUTPUT_BYTES: () => DEFAULT_MIN_OUTPUT_BYTES
50822
51182
  });
50823
51183
  import { spawn as spawn2 } from "child_process";
@@ -50890,6 +51250,13 @@ function classifyRunOutput(opts) {
50890
51250
  }
50891
51251
  return null;
50892
51252
  }
51253
+ function meaningfulStderr(stderr) {
51254
+ if (!stderr)
51255
+ return "";
51256
+ return stderr.split(`
51257
+ `).filter((line) => line.trim().length > 0).filter((line) => !BENIGN_STDERR_PATTERNS.some((re) => re.test(line))).join(`
51258
+ `).trim();
51259
+ }
50893
51260
  function persistErrorLog(errorLogPath, header, stderr, stdoutTail) {
50894
51261
  const parts = [`=== ${redactSecrets(header)} ===`, ""];
50895
51262
  parts.push("--- stderr ---", stderr.trim() ? redactSecrets(stderr) : "(empty)", "");
@@ -51001,13 +51368,40 @@ async function runModels(sessionPath, opts = {}) {
51001
51368
  const minOutputBytes = opts.minOutputBytes ?? DEFAULT_MIN_OUTPUT_BYTES;
51002
51369
  const requirePattern = opts.requirePattern;
51003
51370
  const captureMode = resolveCaptureMode(opts.captureMode);
51371
+ function reconcileTimedOutOutput(id, finalBytes, stdoutTail, stderr) {
51372
+ const current = statusCache.models[id];
51373
+ if (!current || current.state !== "TIMEOUT")
51374
+ return;
51375
+ if (finalBytes <= current.outputSize)
51376
+ return;
51377
+ const degraded = classifyRunOutput({
51378
+ outputSize: finalBytes,
51379
+ stdoutTail,
51380
+ stderr,
51381
+ minOutputBytes
51382
+ });
51383
+ if (degraded) {
51384
+ updateModelStatus(id, { outputSize: finalBytes });
51385
+ return;
51386
+ }
51387
+ updateModelStatus(id, {
51388
+ state: "COMPLETED",
51389
+ outputSize: finalBytes,
51390
+ completedAt: new Date().toISOString(),
51391
+ error: undefined
51392
+ });
51393
+ const note = `Recovered after the deadline: the child flushed ${finalBytes} B while shutting down, ` + "so its answer is complete and is being counted. The run still exceeded its timeout.";
51394
+ const rt = runtimes.get(id);
51395
+ if (rt)
51396
+ persistErrorLog(rt.errorLogPath, `RECOVERED: ${note}`, stderr, stdoutTail);
51397
+ opts.onStatusChange?.(id, statusCache.models[id]);
51398
+ }
51004
51399
  mkdirSync12(statsDir(sessionPath), { recursive: true });
51005
51400
  const processes = new Map;
51006
51401
  const runtimes = new Map;
51007
51402
  const sigintHandler = () => {
51008
51403
  for (const [, proc] of processes) {
51009
- if (!proc.killed)
51010
- proc.kill("SIGTERM");
51404
+ signalProcessTree(proc, "SIGTERM");
51011
51405
  }
51012
51406
  process.exit(1);
51013
51407
  };
@@ -51033,6 +51427,7 @@ async function runModels(sessionPath, opts = {}) {
51033
51427
  const proc = spawn2(teamSpawnTarget.command, [...teamSpawnTarget.prefixArgs, ...args], {
51034
51428
  stdio: ["pipe", "pipe", "pipe"],
51035
51429
  shell: false,
51430
+ detached: KILL_PROCESS_GROUP,
51036
51431
  env: {
51037
51432
  ...process.env,
51038
51433
  CLAUDISH_TOKEN_FILE: tokenFileFor(sessionPath, anonId)
@@ -51093,6 +51488,7 @@ async function runModels(sessionPath, opts = {}) {
51093
51488
  return;
51094
51489
  if (statusCache.models[anonId].state === "TIMEOUT") {
51095
51490
  resolved = true;
51491
+ reconcileTimedOutOutput(anonId, byteCount, stdoutTail, stderr);
51096
51492
  resolve4();
51097
51493
  return;
51098
51494
  }
@@ -51150,13 +51546,8 @@ async function runModels(sessionPath, opts = {}) {
51150
51546
  };
51151
51547
  outputStream.on("close", finish);
51152
51548
  proc.on("exit", (code) => {
51153
- const current = statusCache.models[anonId];
51154
- if (current?.state === "TIMEOUT") {
51155
- resolved = true;
51156
- resolve4();
51157
- return;
51158
- }
51159
- if (stderr) {
51549
+ const timedOut = statusCache.models[anonId]?.state === "TIMEOUT";
51550
+ if (!timedOut && meaningfulStderr(stderr)) {
51160
51551
  writeFileSync12(errorLogPath, redactSecrets(stderr), "utf-8");
51161
51552
  }
51162
51553
  exitCode = code;
@@ -51198,48 +51589,98 @@ async function runModels(sessionPath, opts = {}) {
51198
51589
  emitProgress();
51199
51590
  const progressHandle = setInterval(() => emitProgress("running"), POLL_MS);
51200
51591
  progressHandle.unref?.();
51201
- let timeoutHandle = null;
51202
- await Promise.race([
51203
- Promise.all(completionPromises),
51204
- new Promise((resolve4) => {
51205
- timeoutHandle = setTimeout(() => {
51206
- for (const [id, proc] of processes) {
51207
- const current = statusCache.models[id];
51208
- if (current.state === "RUNNING") {
51209
- if (!proc.killed)
51210
- proc.kill("SIGTERM");
51211
- const rt = runtimes.get(id);
51212
- rt?.flushPartial();
51213
- const stderr = rt?.getStderr() ?? "";
51214
- const stdoutTail = rt?.getStdoutTail() ?? "";
51215
- const bytes2 = rt?.getByteCount() ?? 0;
51216
- const detail = `Killed by the orchestrator after ${timeoutMs / 1000}s with ${bytes2} B of stdout. ` + "That figure counts the ANSWER, not the wire format, so 0 B means the child had " + `not produced an assistant message yet \u2014 "did not finish", not "produced nothing".`;
51217
- if (rt)
51218
- persistErrorLog(rt.errorLogPath, `TIMEOUT: ${detail}`, stderr, stdoutTail);
51219
- updateModelStatus(id, {
51220
- state: "TIMEOUT",
51221
- completedAt: new Date().toISOString(),
51222
- outputSize: bytes2,
51223
- error: rt ? {
51224
- model: id,
51225
- command: rt.command,
51226
- reason: "timeout",
51227
- detail,
51228
- stderrSnippet: stderr ? redactSecrets(stderr).slice(-2000) : undefined,
51229
- stdoutSnippet: stdoutTail ? redactSecrets(stdoutTail).slice(-2000) : undefined,
51230
- errorLogPath: rt.errorLogPath,
51231
- workDir: sessionPath
51232
- } : undefined
51233
- });
51234
- opts.onStatusChange?.(id, statusCache.models[id]);
51235
- }
51592
+ const graceEnabled = opts.graceExtension ?? true;
51593
+ const maxGraceMs = Math.max(0, (opts.maxGraceSeconds ?? timeoutMs / 1000) * 1000);
51594
+ const stallMs = Math.max(0, (opts.stallSeconds ?? DEFAULT_STALL_SECONDS) * 1000);
51595
+ const idleMsFor = (id) => {
51596
+ const s = readTokenStats(sessionPath, id);
51597
+ if (!s || typeof s.updated_at !== "number" || s.updated_at <= 0)
51598
+ return null;
51599
+ return Math.max(0, Date.now() - s.updated_at);
51600
+ };
51601
+ const graceStartedAt = new Map;
51602
+ const graceUsedMs = (id, now) => {
51603
+ const start = graceStartedAt.get(id);
51604
+ return start === undefined ? 0 : Math.max(0, now - start);
51605
+ };
51606
+ const runningIds = () => [...processes.keys()].filter((id) => statusCache.models[id]?.state === "RUNNING");
51607
+ const timeoutModel = async (id, why) => {
51608
+ const proc = processes.get(id);
51609
+ if (!proc || statusCache.models[id]?.state !== "RUNNING")
51610
+ return;
51611
+ const rt = runtimes.get(id);
51612
+ rt?.flushPartial();
51613
+ const stderr = rt?.getStderr() ?? "";
51614
+ const stdoutTail = rt?.getStdoutTail() ?? "";
51615
+ const bytes2 = rt?.getByteCount() ?? 0;
51616
+ const grace = graceUsedMs(id, Date.now());
51617
+ const detail = `Killed by the orchestrator after ${(timeoutMs + grace) / 1000}s ` + `(deadline ${timeoutMs / 1000}s${grace ? ` + ${grace / 1000}s grace` : ""}) ` + `with ${bytes2} B of stdout \u2014 ${why}. ` + "That figure counts the ANSWER, not the wire format, so 0 B means the child had " + `not produced an assistant message yet \u2014 "did not finish", not "produced nothing".`;
51618
+ if (rt)
51619
+ persistErrorLog(rt.errorLogPath, `TIMEOUT: ${detail}`, stderr, stdoutTail);
51620
+ updateModelStatus(id, {
51621
+ state: "TIMEOUT",
51622
+ completedAt: new Date().toISOString(),
51623
+ outputSize: bytes2,
51624
+ error: rt ? {
51625
+ model: id,
51626
+ command: rt.command,
51627
+ reason: "timeout",
51628
+ detail,
51629
+ stderrSnippet: stderr ? redactSecrets(stderr).slice(-2000) : undefined,
51630
+ stdoutSnippet: stdoutTail ? redactSecrets(stdoutTail).slice(-2000) : undefined,
51631
+ errorLogPath: rt.errorLogPath,
51632
+ workDir: sessionPath
51633
+ } : undefined
51634
+ });
51635
+ opts.onStatusChange?.(id, statusCache.models[id]);
51636
+ const stopped = await terminateChildTree(proc);
51637
+ if (!stopped) {
51638
+ persistErrorLog(rt?.errorLogPath ?? join28(sessionPath, "errors", `${id}.log`), "TIMEOUT: child survived SIGKILL \u2014 it may still be running and billing", stderr, stdoutTail);
51639
+ }
51640
+ };
51641
+ const allDone = Promise.all(completionPromises);
51642
+ let settled = false;
51643
+ allDone.then(() => {
51644
+ settled = true;
51645
+ }, () => {
51646
+ settled = true;
51647
+ });
51648
+ const deadlineWatcher = (async () => {
51649
+ await delay(timeoutMs);
51650
+ for (;; ) {
51651
+ if (settled)
51652
+ return;
51653
+ const running = runningIds();
51654
+ if (running.length === 0)
51655
+ return;
51656
+ const extended = [];
51657
+ const now = Date.now();
51658
+ for (const id of running) {
51659
+ const idleMs = idleMsFor(id);
51660
+ const usedGrace = graceUsedMs(id, now);
51661
+ if (!graceEnabled) {
51662
+ await timeoutModel(id, "deadline reached (grace extension disabled)");
51663
+ } else if (usedGrace >= maxGraceMs) {
51664
+ await timeoutModel(id, `grace exhausted after ${Math.round(usedGrace / 1000)}s of extra time`);
51665
+ } else if (idleMs === null) {
51666
+ await timeoutModel(id, "deadline reached with no measurable progress to extend for");
51667
+ } else if (idleMs >= stallMs) {
51668
+ await timeoutModel(id, `no measurable progress for ${Math.round(idleMs / 1000)}s`);
51669
+ } else {
51670
+ if (!graceStartedAt.has(id))
51671
+ graceStartedAt.set(id, now);
51672
+ extended.push(id);
51236
51673
  }
51237
- resolve4();
51238
- }, timeoutMs);
51239
- })
51240
- ]);
51241
- if (timeoutHandle !== null)
51242
- clearTimeout(timeoutHandle);
51674
+ }
51675
+ if (extended.length === 0)
51676
+ return;
51677
+ emitProgress("running");
51678
+ await delay(Math.min(GRACE_INTERVAL_MS, Math.max(1000, stallMs)));
51679
+ }
51680
+ })().catch(() => {});
51681
+ await Promise.race([allDone, deadlineWatcher]);
51682
+ if (!settled)
51683
+ await Promise.race([allDone, delay(DRAIN_TIMEOUT_MS)]);
51243
51684
  clearInterval(progressHandle);
51244
51685
  emitProgress("settled");
51245
51686
  process.off("SIGINT", sigintHandler);
@@ -51425,14 +51866,19 @@ function formatVerdict(verdict, sessionPath) {
51425
51866
  }
51426
51867
  return output;
51427
51868
  }
51428
- var TEAM_CAPTURE_ENV_VAR = "CLAUDISH_TEAM_CAPTURE", STDOUT_TAIL_LIMIT = 4000, API_ERROR_RE, BG_CEILING_RE, DEFAULT_MIN_OUTPUT_BYTES = 0, SENTINEL_MODELS;
51869
+ var TEAM_CAPTURE_ENV_VAR = "CLAUDISH_TEAM_CAPTURE", STDOUT_TAIL_LIMIT = 4000, API_ERROR_RE, BG_CEILING_RE, DEFAULT_MIN_OUTPUT_BYTES = 0, DRAIN_TIMEOUT_MS = 1e4, GRACE_INTERVAL_MS = 60000, DEFAULT_STALL_SECONDS = 90, delay = (ms) => new Promise((resolve4) => {
51870
+ const t = setTimeout(resolve4, ms);
51871
+ t.unref?.();
51872
+ }), BENIGN_STDERR_PATTERNS, SENTINEL_MODELS;
51429
51873
  var init_team_orchestrator = __esm(() => {
51430
51874
  init_prehydrate();
51875
+ init_process_tree();
51431
51876
  init_redact();
51432
51877
  init_team_stats();
51433
51878
  init_team_stream_capture();
51434
51879
  API_ERROR_RE = /\[API Error:\s*([^\]]{0,300})\]/i;
51435
51880
  BG_CEILING_RE = /Background tasks still running after (\d+)s; terminating/i;
51881
+ BENIGN_STDERR_PATTERNS = [/^\s*\[claude-code:unrecognized_model\]/];
51436
51882
  SENTINEL_MODELS = new Set([
51437
51883
  "internal",
51438
51884
  "default",
@@ -52976,6 +53422,11 @@ function findMagmuxBinary() {
52976
53422
  throw new Error(`magmux not found. Install it:
52977
53423
  brew install MadAppGang/tap/magmux`);
52978
53424
  }
53425
+ function withoutControlPanes(evt) {
53426
+ if (!Array.isArray(evt.panes))
53427
+ return evt;
53428
+ return { ...evt, panes: evt.panes.filter((p) => p?.control !== true) };
53429
+ }
52979
53430
  async function subscribeToMagmux(sockPath, onEvent) {
52980
53431
  let client = null;
52981
53432
  for (let attempt = 0;attempt < 40; attempt++) {
@@ -53012,7 +53463,7 @@ async function subscribeToMagmux(sockPath, onEvent) {
53012
53463
  const evt = JSON.parse(line);
53013
53464
  onEvent?.(evt);
53014
53465
  if (evt.type === "results") {
53015
- finalResults = evt;
53466
+ finalResults = withoutControlPanes(evt);
53016
53467
  }
53017
53468
  } catch {}
53018
53469
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudish",
3
- "version": "7.51.0",
3
+ "version": "7.53.0",
4
4
  "description": "Run Claude Code with any model - OpenRouter, Ollama, LM Studio & local models",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -60,10 +60,10 @@
60
60
  "ai"
61
61
  ],
62
62
  "optionalDependencies": {
63
- "@claudish/magmux-darwin-arm64": "7.51.0",
64
- "@claudish/magmux-darwin-x64": "7.51.0",
65
- "@claudish/magmux-linux-arm64": "7.51.0",
66
- "@claudish/magmux-linux-x64": "7.51.0"
63
+ "@claudish/magmux-darwin-arm64": "7.53.0",
64
+ "@claudish/magmux-darwin-x64": "7.53.0",
65
+ "@claudish/magmux-linux-arm64": "7.53.0",
66
+ "@claudish/magmux-linux-x64": "7.53.0"
67
67
  },
68
68
  "author": "Jack Rudenko <i@madappgang.com>",
69
69
  "license": "MIT",