claudish 7.51.0 → 7.52.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.52.0";
733
733
 
734
734
  // src/logger.ts
735
735
  var exports_logger = {};
@@ -30776,6 +30776,7 @@ class AntigravityProviderTransport {
30776
30776
  if (!data?.buckets?.length)
30777
30777
  return;
30778
30778
  const lines = [];
30779
+ let sawThrottledModel = false;
30779
30780
  for (const bucket of data.buckets) {
30780
30781
  if (!bucket.modelId)
30781
30782
  continue;
@@ -30784,15 +30785,24 @@ class AntigravityProviderTransport {
30784
30785
  hour: "2-digit",
30785
30786
  minute: "2-digit"
30786
30787
  }) : "?";
30787
- lines.push(` ${bucket.modelId}: ${pct} remaining (resets ${reset})`);
30788
+ const mine = this.isThrottledModel(bucket.modelId);
30789
+ if (mine)
30790
+ sawThrottledModel = true;
30791
+ lines.push(` ${mine ? "> " : " "}${bucket.modelId}: ${pct} remaining (resets ${reset})`);
30788
30792
  }
30789
- if (lines.length > 0) {
30790
- logStderr(`[Antigravity] Quota status:
30793
+ if (lines.length === 0)
30794
+ return;
30795
+ const requested = this.servedModelName && this.servedModelName !== this.modelName ? `${this.modelName} (served as ${this.servedModelName})` : this.modelName;
30796
+ 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:";
30797
+ logStderr(`${header}
30791
30798
  ${lines.join(`
30792
30799
  `)}`);
30793
- }
30794
30800
  } catch {}
30795
30801
  }
30802
+ isThrottledModel(bucketModelId) {
30803
+ const id = bucketModelId.toLowerCase();
30804
+ return id === this.modelName.toLowerCase() || id === this.servedModelName.toLowerCase();
30805
+ }
30796
30806
  async getQuotaRemaining(modelName) {
30797
30807
  if (!this.accessToken || !this.projectId)
30798
30808
  return;
@@ -32675,12 +32685,14 @@ var init_deepseek_model_dialect = __esm(() => {
32675
32685
  }
32676
32686
  applyNativeReasoning(request, originalRequest) {
32677
32687
  const effort = this.resolveEffortLevel(originalRequest);
32678
- if (effort && this.isV4Model()) {
32688
+ if (effort && this.acceptsReasoningControls()) {
32679
32689
  if (effort === "none" || effort === "minimal") {
32680
32690
  request.thinking = { type: "disabled" };
32681
32691
  log(`[DeepSeekModelDialect] effort ${effort} -> thinking.type: disabled for ${this.modelId}`);
32682
32692
  } else {
32683
- const value = effort === "xhigh" || effort === "max" ? "max" : "high";
32693
+ const reasoning = this.lookupReasoningCapability();
32694
+ const clamped = reasoning?.control === "effort" && reasoning.efforts?.length ? this.clampToAdvertisedEffort(effort, reasoning) : undefined;
32695
+ const value = clamped ?? (effort === "xhigh" || effort === "max" ? "max" : "high");
32684
32696
  request.reasoning_effort = value;
32685
32697
  log(`[DeepSeekModelDialect] effort ${effort} -> reasoning_effort: ${value} for ${this.modelId}`);
32686
32698
  }
@@ -32692,9 +32704,15 @@ var init_deepseek_model_dialect = __esm(() => {
32692
32704
  }
32693
32705
  return request;
32694
32706
  }
32695
- isV4Model() {
32707
+ acceptsReasoningControls() {
32708
+ const reasoning = this.lookupReasoningCapability();
32709
+ if (reasoning)
32710
+ return reasoning.supported === true && reasoning.control === "effort";
32696
32711
  const model = this.modelId.toLowerCase();
32697
- return model.includes("v4") || model.includes("deepseek-chat") || model.includes("deepseek-reasoner");
32712
+ if (model.includes("deepseek-chat") || model.includes("deepseek-reasoner"))
32713
+ return true;
32714
+ const version2 = /(?:^|[^a-z0-9])v(\d+)/.exec(model);
32715
+ return version2 ? Number(version2[1]) >= 4 : false;
32698
32716
  }
32699
32717
  shouldHandle(modelId) {
32700
32718
  return matchesModelFamily(modelId, "deepseek");
@@ -34105,11 +34123,87 @@ var init_glm_model_dialect = __esm(() => {
34105
34123
  };
34106
34124
  });
34107
34125
 
34126
+ // src/adapters/grok-effort-support.ts
34127
+ function acceptsReasoningEffort(modelId) {
34128
+ const id = norm(modelId);
34129
+ if (!id)
34130
+ return false;
34131
+ if (learnedRejects.has(id))
34132
+ return false;
34133
+ const reasoning = lookupModelReasoning(id) ?? lookupModelReasoning(modelId);
34134
+ if (reasoning)
34135
+ return reasoning.supported === true && reasoning.control === "effort";
34136
+ return !SEED_REJECTS.some((re) => re.test(id));
34137
+ }
34138
+ function catalogReasoningFor(modelId) {
34139
+ return lookupModelReasoning(norm(modelId)) ?? lookupModelReasoning(modelId);
34140
+ }
34141
+ function rememberReasoningEffortRejected(modelId) {
34142
+ const id = norm(modelId);
34143
+ if (!id || learnedRejects.has(id))
34144
+ return;
34145
+ learnedRejects.add(id);
34146
+ log(`[GrokEffortSupport] ${id} rejected reasoning_effort \u2014 not sending it again this session.`);
34147
+ }
34148
+ function isReasoningEffortRejection(errorText) {
34149
+ if (!errorText)
34150
+ return false;
34151
+ return /does not support parameter\s+`?reasoning[_]?effort`?/i.test(errorText);
34152
+ }
34153
+ function acceptsReasoningEffortValue(modelId, value) {
34154
+ return !learnedValueRejects.has(pairKey(modelId, value));
34155
+ }
34156
+ function rememberReasoningEffortValueRejected(modelId, value) {
34157
+ const key = pairKey(modelId, value);
34158
+ if (learnedValueRejects.has(key))
34159
+ return;
34160
+ learnedValueRejects.add(key);
34161
+ log(`[GrokEffortSupport] ${norm(modelId)} rejected reasoning_effort value "${value}" \u2014 ` + "falling back and not sending it again this session.");
34162
+ }
34163
+ function rejectedReasoningEffortValue(errorText) {
34164
+ if (!errorText)
34165
+ return null;
34166
+ const m = /does not support\s+`?reasoning[_]?effort`?\s+value\s+`?([a-z]+)`?/i.exec(errorText);
34167
+ return m ? m[1].toLowerCase() : null;
34168
+ }
34169
+ function fallbackReasoningEffortValue(value) {
34170
+ switch (value.toLowerCase()) {
34171
+ case "none":
34172
+ return "low";
34173
+ case "minimal":
34174
+ return "low";
34175
+ case "low":
34176
+ return null;
34177
+ case "medium":
34178
+ return "low";
34179
+ case "high":
34180
+ return "medium";
34181
+ default:
34182
+ return null;
34183
+ }
34184
+ }
34185
+ var SEED_REJECTS, learnedRejects, norm = (modelId) => modelId.trim().toLowerCase().replace(/^x-ai\//, ""), learnedValueRejects, pairKey = (modelId, value) => `${norm(modelId)}\x00${value.toLowerCase()}`;
34186
+ var init_grok_effort_support = __esm(() => {
34187
+ init_logger();
34188
+ init_model_catalog();
34189
+ SEED_REJECTS = [
34190
+ /non-reasoning/i,
34191
+ /^grok-2/i,
34192
+ /^grok-4(?![.\d])(?!.*fast-reasoning)/i,
34193
+ /^grok-build-/i,
34194
+ /^grok-code-/i,
34195
+ /^grok-4\.20/i
34196
+ ];
34197
+ learnedRejects = new Set;
34198
+ learnedValueRejects = new Set;
34199
+ });
34200
+
34108
34201
  // src/adapters/grok-model-dialect.ts
34109
34202
  var GrokModelDialect;
34110
34203
  var init_grok_model_dialect = __esm(() => {
34111
34204
  init_logger();
34112
34205
  init_base_api_format();
34206
+ init_grok_effort_support();
34113
34207
  init_model_catalog();
34114
34208
  GrokModelDialect = class GrokModelDialect extends BaseAPIFormat {
34115
34209
  xmlBuffer = "";
@@ -34172,34 +34266,78 @@ var init_grok_model_dialect = __esm(() => {
34172
34266
  return request;
34173
34267
  }
34174
34268
  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)) {
34269
+ if (!acceptsReasoningEffort(this.modelId)) {
34180
34270
  return;
34181
34271
  }
34182
- if (isMini) {
34183
- switch (effort) {
34184
- case "high":
34185
- case "xhigh":
34186
- case "max":
34187
- return "high";
34188
- default:
34189
- return "low";
34272
+ let value;
34273
+ const reasoning = catalogReasoningFor(this.modelId);
34274
+ const clamped = reasoning ? this.clampToAdvertisedEffort(effort, reasoning) : undefined;
34275
+ if (clamped) {
34276
+ value = clamped;
34277
+ } else {
34278
+ const model = this.modelId.toLowerCase();
34279
+ const isMini = model.includes("mini");
34280
+ if (isMini) {
34281
+ switch (effort) {
34282
+ case "high":
34283
+ case "xhigh":
34284
+ case "max":
34285
+ value = "high";
34286
+ break;
34287
+ default:
34288
+ value = "low";
34289
+ }
34290
+ } else {
34291
+ switch (effort) {
34292
+ case "none":
34293
+ value = "none";
34294
+ break;
34295
+ case "minimal":
34296
+ case "low":
34297
+ value = "low";
34298
+ break;
34299
+ case "medium":
34300
+ value = "medium";
34301
+ break;
34302
+ default:
34303
+ value = "high";
34304
+ }
34190
34305
  }
34191
34306
  }
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";
34307
+ let guard = 0;
34308
+ while (!acceptsReasoningEffortValue(this.modelId, value) && guard++ < 8) {
34309
+ const next = fallbackReasoningEffortValue(value);
34310
+ if (!next)
34311
+ return;
34312
+ value = next;
34313
+ }
34314
+ return value;
34315
+ }
34316
+ recoverFromRejection(payload, errorText) {
34317
+ if (!payload || payload.reasoning_effort === undefined)
34318
+ return null;
34319
+ if (isReasoningEffortRejection(errorText)) {
34320
+ rememberReasoningEffortRejected(this.modelId);
34321
+ const next = { ...payload };
34322
+ delete next.reasoning_effort;
34323
+ return { payload: next, note: `dropped reasoning_effort for ${this.modelId}` };
34324
+ }
34325
+ const rejected = rejectedReasoningEffortValue(errorText);
34326
+ if (rejected) {
34327
+ rememberReasoningEffortValueRejected(this.modelId, rejected);
34328
+ const fallback = fallbackReasoningEffortValue(rejected);
34329
+ const next = { ...payload };
34330
+ if (fallback) {
34331
+ next.reasoning_effort = fallback;
34332
+ return {
34333
+ payload: next,
34334
+ note: `reasoning_effort "${rejected}" -> "${fallback}" for ${this.modelId}`
34335
+ };
34336
+ }
34337
+ delete next.reasoning_effort;
34338
+ return { payload: next, note: `dropped unsupported reasoning_effort for ${this.modelId}` };
34202
34339
  }
34340
+ return null;
34203
34341
  }
34204
34342
  parseXmlParameters(xmlContent) {
34205
34343
  const params = {};
@@ -36079,6 +36217,45 @@ Do not invent a different filename, and do not derive one from the task. Claude
36079
36217
  PLAN_MODE_RULES = [planFilePathRule];
36080
36218
  });
36081
36219
 
36220
+ // src/behavior/rules/session-context.ts
36221
+ function hasInjectedSessionContext(systemText2, messages) {
36222
+ const haystacks = [systemText2 ?? ""];
36223
+ const first = Array.isArray(messages) ? messages[0] : undefined;
36224
+ if (first && typeof first === "object") {
36225
+ const content = first.content;
36226
+ if (typeof content === "string") {
36227
+ haystacks.push(content);
36228
+ } else if (Array.isArray(content)) {
36229
+ for (const block of content) {
36230
+ const text = block?.text;
36231
+ if (typeof text === "string")
36232
+ haystacks.push(text);
36233
+ }
36234
+ }
36235
+ }
36236
+ return haystacks.some((h) => {
36237
+ const lower = h.toLowerCase();
36238
+ return SESSION_CONTEXT_MARKERS.some((m) => lower.includes(m));
36239
+ });
36240
+ }
36241
+ var SESSION_CONTEXT_MARKERS, NOTE, noHarnessEchoRule, SESSION_CONTEXT_RULES;
36242
+ var init_session_context = __esm(() => {
36243
+ SESSION_CONTEXT_MARKERS = ["sessionstart hook additional context", "hook additional context"];
36244
+ 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.";
36245
+ noHarnessEchoRule = {
36246
+ id: "session-context/no-harness-echo",
36247
+ description: "Stop foreign models opening their answer with harness-injected session context " + "(SessionStart hook output, coaching/insight blocks).",
36248
+ defaultSeverity: "fix",
36249
+ appliesTo: ({ isNativeAnthropic }) => !isNativeAnthropic,
36250
+ onRequest(ctx) {
36251
+ if (!hasInjectedSessionContext(ctx.systemText, ctx.messages))
36252
+ return [];
36253
+ return [{ type: "injectSystemNote", text: NOTE }];
36254
+ }
36255
+ };
36256
+ SESSION_CONTEXT_RULES = [noHarnessEchoRule];
36257
+ });
36258
+
36082
36259
  // src/behavior/hooks.ts
36083
36260
  import { isAbsolute, resolve } from "path";
36084
36261
  function isBehaviorRule(value) {
@@ -36288,6 +36465,7 @@ var init_behavior = __esm(() => {
36288
36465
  init_config();
36289
36466
  init_engine();
36290
36467
  init_plan_mode();
36468
+ init_session_context();
36291
36469
  init_engine();
36292
36470
  init_config();
36293
36471
  init_harness();
@@ -36297,7 +36475,7 @@ var init_behavior = __esm(() => {
36297
36475
  init_corpus();
36298
36476
  init_aggregate();
36299
36477
  init_upload();
36300
- BUILTIN_RULES = [...PLAN_MODE_RULES];
36478
+ BUILTIN_RULES = [...PLAN_MODE_RULES, ...SESSION_CONTEXT_RULES];
36301
36479
  hookRules = [];
36302
36480
  });
36303
36481
 
@@ -40840,6 +41018,30 @@ class ComposedHandler {
40840
41018
  }
40841
41019
  log(`[${this.provider.displayName}] Response status: ${response.status}`);
40842
41020
  this.capturePlanUsage(response);
41021
+ if (!response.ok) {
41022
+ if (response.status >= 400 && response.status < 500 && this.modelAdapter?.recoverFromRejection) {
41023
+ const errorText = await response.clone().text();
41024
+ const recovery = this.modelAdapter.recoverFromRejection(requestPayload, errorText);
41025
+ if (recovery) {
41026
+ log(`[${this.provider.displayName}] Parameter rejected \u2014 retrying: ${recovery.note}`);
41027
+ requestPayload = recovery.payload;
41028
+ const retrySerialized = this.provider.serializeBody?.(requestPayload);
41029
+ const retryHeaders = await this.provider.getHeaders();
41030
+ retryHeaders["Content-Type"] = retrySerialized?.contentType ?? "application/json";
41031
+ const retryResp = await fetch(endpoint, {
41032
+ method: "POST",
41033
+ headers: retryHeaders,
41034
+ body: retrySerialized?.body ?? JSON.stringify(requestPayload),
41035
+ ...this.provider.getRequestInit?.() || {}
41036
+ });
41037
+ if (retryResp.ok) {
41038
+ response = retryResp;
41039
+ } else {
41040
+ log(`[${this.provider.displayName}] Retry after ${recovery.note} still failed ` + `(HTTP ${retryResp.status})`);
41041
+ }
41042
+ }
41043
+ }
41044
+ }
40843
41045
  if (!response.ok) {
40844
41046
  if (response.status === 401 && this.provider.forceRefreshAuth) {
40845
41047
  log(`[${this.provider.displayName}] Got 401, forcing auth refresh and retrying`);
@@ -44643,6 +44845,48 @@ var init_signal_watcher = __esm(() => {
44643
44845
  QUESTION_PATTERNS = [/\?\s*$/m, /\bchoose\b.*:/im, /\bselect\b.*:/im, /\benter\b.*:/im];
44644
44846
  });
44645
44847
 
44848
+ // src/process-tree.ts
44849
+ function signalProcessTree(proc, signal) {
44850
+ if (KILL_PROCESS_GROUP && proc.pid) {
44851
+ try {
44852
+ process.kill(-proc.pid, signal);
44853
+ } catch {}
44854
+ }
44855
+ try {
44856
+ if (!proc.killed)
44857
+ proc.kill(signal);
44858
+ } catch {}
44859
+ }
44860
+ function waitForExit(proc, ms) {
44861
+ if (proc.exitCode !== null || proc.signalCode !== null)
44862
+ return Promise.resolve(true);
44863
+ return new Promise((resolve2) => {
44864
+ let settled = false;
44865
+ const done = (value) => {
44866
+ if (settled)
44867
+ return;
44868
+ settled = true;
44869
+ proc.off("exit", onExit);
44870
+ resolve2(value);
44871
+ };
44872
+ const onExit = () => done(true);
44873
+ proc.once("exit", onExit);
44874
+ const t = setTimeout(() => done(false), ms);
44875
+ t.unref?.();
44876
+ });
44877
+ }
44878
+ async function terminateChildTree(proc, graceMs = TERMINATE_GRACE_MS) {
44879
+ signalProcessTree(proc, "SIGTERM");
44880
+ if (await waitForExit(proc, graceMs))
44881
+ return true;
44882
+ signalProcessTree(proc, "SIGKILL");
44883
+ return waitForExit(proc, graceMs);
44884
+ }
44885
+ var KILL_PROCESS_GROUP, TERMINATE_GRACE_MS = 5000;
44886
+ var init_process_tree = __esm(() => {
44887
+ KILL_PROCESS_GROUP = process.platform !== "win32";
44888
+ });
44889
+
44646
44890
  // src/spawn-claudish.ts
44647
44891
  function resolveClaudishSpawn(env = process.env) {
44648
44892
  const bin = env[CLAUDISH_BIN_ENV]?.trim();
@@ -44699,7 +44943,8 @@ class SessionManager {
44699
44943
  const proc = spawn(spawnTarget.command, [...spawnTarget.prefixArgs, ...args], {
44700
44944
  cwd: opts.cwd ?? process.cwd(),
44701
44945
  stdio: ["pipe", "pipe", "pipe"],
44702
- shell: false
44946
+ shell: false,
44947
+ detached: KILL_PROCESS_GROUP
44703
44948
  });
44704
44949
  const scrollback = new ScrollbackBuffer(this.scrollbackCapacity);
44705
44950
  const watcher = new SignalWatcher(sessionId2, (sid, data) => {
@@ -44779,10 +45024,10 @@ class SessionManager {
44779
45024
  });
44780
45025
  entry.timeoutHandle = setTimeout(() => {
44781
45026
  if (!proc.killed) {
44782
- proc.kill("SIGTERM");
45027
+ signalProcessTree(proc, "SIGTERM");
44783
45028
  entry.killHandle = setTimeout(() => {
44784
45029
  try {
44785
- proc.kill("SIGKILL");
45030
+ signalProcessTree(proc, "SIGKILL");
44786
45031
  } catch {}
44787
45032
  }, KILL_GRACE_MS);
44788
45033
  entry.info.status = "timeout";
@@ -44840,10 +45085,10 @@ class SessionManager {
44840
45085
  entry.info.completedAt = new Date().toISOString();
44841
45086
  entry.watcher.forceState("cancelled", "Session cancelled");
44842
45087
  if (!entry.process.killed) {
44843
- entry.process.kill("SIGTERM");
45088
+ signalProcessTree(entry.process, "SIGTERM");
44844
45089
  entry.killHandle = setTimeout(() => {
44845
45090
  try {
44846
- entry.process.kill("SIGKILL");
45091
+ signalProcessTree(entry.process, "SIGKILL");
44847
45092
  } catch {}
44848
45093
  }, KILL_GRACE_MS);
44849
45094
  }
@@ -44874,11 +45119,11 @@ class SessionManager {
44874
45119
  const promises = [];
44875
45120
  for (const [, entry] of this.sessions) {
44876
45121
  if (!entry.process.killed) {
44877
- entry.process.kill("SIGTERM");
45122
+ signalProcessTree(entry.process, "SIGTERM");
44878
45123
  promises.push(new Promise((resolve2) => {
44879
45124
  const timeout = setTimeout(() => {
44880
45125
  try {
44881
- entry.process.kill("SIGKILL");
45126
+ signalProcessTree(entry.process, "SIGKILL");
44882
45127
  } catch {}
44883
45128
  resolve2();
44884
45129
  }, KILL_GRACE_MS);
@@ -44924,6 +45169,7 @@ class SessionManager {
44924
45169
  }
44925
45170
  var DEFAULT_MAX_SESSIONS = 20, DEFAULT_SCROLLBACK = 2000, DEFAULT_TIMEOUT = 600, MAX_TIMEOUT = 3600, KILL_GRACE_MS = 5000;
44926
45171
  var init_session_manager = __esm(() => {
45172
+ init_process_tree();
44927
45173
  init_scrollback_buffer();
44928
45174
  init_signal_watcher();
44929
45175
  });
@@ -50810,6 +51056,7 @@ __export(exports_team_orchestrator, {
50810
51056
  runModels: () => runModels,
50811
51057
  resolveCaptureMode: () => resolveCaptureMode,
50812
51058
  parseJudgeVotes: () => parseJudgeVotes,
51059
+ meaningfulStderr: () => meaningfulStderr,
50813
51060
  judgeResponses: () => judgeResponses,
50814
51061
  getStatus: () => getStatus,
50815
51062
  fisherYatesShuffle: () => fisherYatesShuffle,
@@ -50818,6 +51065,9 @@ __export(exports_team_orchestrator, {
50818
51065
  aggregateVerdict: () => aggregateVerdict,
50819
51066
  TEAM_CAPTURE_ENV_VAR: () => TEAM_CAPTURE_ENV_VAR,
50820
51067
  STDOUT_TAIL_LIMIT: () => STDOUT_TAIL_LIMIT,
51068
+ GRACE_INTERVAL_MS: () => GRACE_INTERVAL_MS,
51069
+ DRAIN_TIMEOUT_MS: () => DRAIN_TIMEOUT_MS,
51070
+ DEFAULT_STALL_SECONDS: () => DEFAULT_STALL_SECONDS,
50821
51071
  DEFAULT_MIN_OUTPUT_BYTES: () => DEFAULT_MIN_OUTPUT_BYTES
50822
51072
  });
50823
51073
  import { spawn as spawn2 } from "child_process";
@@ -50890,6 +51140,13 @@ function classifyRunOutput(opts) {
50890
51140
  }
50891
51141
  return null;
50892
51142
  }
51143
+ function meaningfulStderr(stderr) {
51144
+ if (!stderr)
51145
+ return "";
51146
+ return stderr.split(`
51147
+ `).filter((line) => line.trim().length > 0).filter((line) => !BENIGN_STDERR_PATTERNS.some((re) => re.test(line))).join(`
51148
+ `).trim();
51149
+ }
50893
51150
  function persistErrorLog(errorLogPath, header, stderr, stdoutTail) {
50894
51151
  const parts = [`=== ${redactSecrets(header)} ===`, ""];
50895
51152
  parts.push("--- stderr ---", stderr.trim() ? redactSecrets(stderr) : "(empty)", "");
@@ -51001,13 +51258,40 @@ async function runModels(sessionPath, opts = {}) {
51001
51258
  const minOutputBytes = opts.minOutputBytes ?? DEFAULT_MIN_OUTPUT_BYTES;
51002
51259
  const requirePattern = opts.requirePattern;
51003
51260
  const captureMode = resolveCaptureMode(opts.captureMode);
51261
+ function reconcileTimedOutOutput(id, finalBytes, stdoutTail, stderr) {
51262
+ const current = statusCache.models[id];
51263
+ if (!current || current.state !== "TIMEOUT")
51264
+ return;
51265
+ if (finalBytes <= current.outputSize)
51266
+ return;
51267
+ const degraded = classifyRunOutput({
51268
+ outputSize: finalBytes,
51269
+ stdoutTail,
51270
+ stderr,
51271
+ minOutputBytes
51272
+ });
51273
+ if (degraded) {
51274
+ updateModelStatus(id, { outputSize: finalBytes });
51275
+ return;
51276
+ }
51277
+ updateModelStatus(id, {
51278
+ state: "COMPLETED",
51279
+ outputSize: finalBytes,
51280
+ completedAt: new Date().toISOString(),
51281
+ error: undefined
51282
+ });
51283
+ 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.";
51284
+ const rt = runtimes.get(id);
51285
+ if (rt)
51286
+ persistErrorLog(rt.errorLogPath, `RECOVERED: ${note}`, stderr, stdoutTail);
51287
+ opts.onStatusChange?.(id, statusCache.models[id]);
51288
+ }
51004
51289
  mkdirSync12(statsDir(sessionPath), { recursive: true });
51005
51290
  const processes = new Map;
51006
51291
  const runtimes = new Map;
51007
51292
  const sigintHandler = () => {
51008
51293
  for (const [, proc] of processes) {
51009
- if (!proc.killed)
51010
- proc.kill("SIGTERM");
51294
+ signalProcessTree(proc, "SIGTERM");
51011
51295
  }
51012
51296
  process.exit(1);
51013
51297
  };
@@ -51033,6 +51317,7 @@ async function runModels(sessionPath, opts = {}) {
51033
51317
  const proc = spawn2(teamSpawnTarget.command, [...teamSpawnTarget.prefixArgs, ...args], {
51034
51318
  stdio: ["pipe", "pipe", "pipe"],
51035
51319
  shell: false,
51320
+ detached: KILL_PROCESS_GROUP,
51036
51321
  env: {
51037
51322
  ...process.env,
51038
51323
  CLAUDISH_TOKEN_FILE: tokenFileFor(sessionPath, anonId)
@@ -51093,6 +51378,7 @@ async function runModels(sessionPath, opts = {}) {
51093
51378
  return;
51094
51379
  if (statusCache.models[anonId].state === "TIMEOUT") {
51095
51380
  resolved = true;
51381
+ reconcileTimedOutOutput(anonId, byteCount, stdoutTail, stderr);
51096
51382
  resolve4();
51097
51383
  return;
51098
51384
  }
@@ -51150,13 +51436,8 @@ async function runModels(sessionPath, opts = {}) {
51150
51436
  };
51151
51437
  outputStream.on("close", finish);
51152
51438
  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) {
51439
+ const timedOut = statusCache.models[anonId]?.state === "TIMEOUT";
51440
+ if (!timedOut && meaningfulStderr(stderr)) {
51160
51441
  writeFileSync12(errorLogPath, redactSecrets(stderr), "utf-8");
51161
51442
  }
51162
51443
  exitCode = code;
@@ -51198,48 +51479,98 @@ async function runModels(sessionPath, opts = {}) {
51198
51479
  emitProgress();
51199
51480
  const progressHandle = setInterval(() => emitProgress("running"), POLL_MS);
51200
51481
  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
- }
51482
+ const graceEnabled = opts.graceExtension ?? true;
51483
+ const maxGraceMs = Math.max(0, (opts.maxGraceSeconds ?? timeoutMs / 1000) * 1000);
51484
+ const stallMs = Math.max(0, (opts.stallSeconds ?? DEFAULT_STALL_SECONDS) * 1000);
51485
+ const idleMsFor = (id) => {
51486
+ const s = readTokenStats(sessionPath, id);
51487
+ if (!s || typeof s.updated_at !== "number" || s.updated_at <= 0)
51488
+ return null;
51489
+ return Math.max(0, Date.now() - s.updated_at);
51490
+ };
51491
+ const graceStartedAt = new Map;
51492
+ const graceUsedMs = (id, now) => {
51493
+ const start = graceStartedAt.get(id);
51494
+ return start === undefined ? 0 : Math.max(0, now - start);
51495
+ };
51496
+ const runningIds = () => [...processes.keys()].filter((id) => statusCache.models[id]?.state === "RUNNING");
51497
+ const timeoutModel = async (id, why) => {
51498
+ const proc = processes.get(id);
51499
+ if (!proc || statusCache.models[id]?.state !== "RUNNING")
51500
+ return;
51501
+ const rt = runtimes.get(id);
51502
+ rt?.flushPartial();
51503
+ const stderr = rt?.getStderr() ?? "";
51504
+ const stdoutTail = rt?.getStdoutTail() ?? "";
51505
+ const bytes2 = rt?.getByteCount() ?? 0;
51506
+ const grace = graceUsedMs(id, Date.now());
51507
+ 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".`;
51508
+ if (rt)
51509
+ persistErrorLog(rt.errorLogPath, `TIMEOUT: ${detail}`, stderr, stdoutTail);
51510
+ updateModelStatus(id, {
51511
+ state: "TIMEOUT",
51512
+ completedAt: new Date().toISOString(),
51513
+ outputSize: bytes2,
51514
+ error: rt ? {
51515
+ model: id,
51516
+ command: rt.command,
51517
+ reason: "timeout",
51518
+ detail,
51519
+ stderrSnippet: stderr ? redactSecrets(stderr).slice(-2000) : undefined,
51520
+ stdoutSnippet: stdoutTail ? redactSecrets(stdoutTail).slice(-2000) : undefined,
51521
+ errorLogPath: rt.errorLogPath,
51522
+ workDir: sessionPath
51523
+ } : undefined
51524
+ });
51525
+ opts.onStatusChange?.(id, statusCache.models[id]);
51526
+ const stopped = await terminateChildTree(proc);
51527
+ if (!stopped) {
51528
+ persistErrorLog(rt?.errorLogPath ?? join28(sessionPath, "errors", `${id}.log`), "TIMEOUT: child survived SIGKILL \u2014 it may still be running and billing", stderr, stdoutTail);
51529
+ }
51530
+ };
51531
+ const allDone = Promise.all(completionPromises);
51532
+ let settled = false;
51533
+ allDone.then(() => {
51534
+ settled = true;
51535
+ }, () => {
51536
+ settled = true;
51537
+ });
51538
+ const deadlineWatcher = (async () => {
51539
+ await delay(timeoutMs);
51540
+ for (;; ) {
51541
+ if (settled)
51542
+ return;
51543
+ const running = runningIds();
51544
+ if (running.length === 0)
51545
+ return;
51546
+ const extended = [];
51547
+ const now = Date.now();
51548
+ for (const id of running) {
51549
+ const idleMs = idleMsFor(id);
51550
+ const usedGrace = graceUsedMs(id, now);
51551
+ if (!graceEnabled) {
51552
+ await timeoutModel(id, "deadline reached (grace extension disabled)");
51553
+ } else if (usedGrace >= maxGraceMs) {
51554
+ await timeoutModel(id, `grace exhausted after ${Math.round(usedGrace / 1000)}s of extra time`);
51555
+ } else if (idleMs === null) {
51556
+ await timeoutModel(id, "deadline reached with no measurable progress to extend for");
51557
+ } else if (idleMs >= stallMs) {
51558
+ await timeoutModel(id, `no measurable progress for ${Math.round(idleMs / 1000)}s`);
51559
+ } else {
51560
+ if (!graceStartedAt.has(id))
51561
+ graceStartedAt.set(id, now);
51562
+ extended.push(id);
51236
51563
  }
51237
- resolve4();
51238
- }, timeoutMs);
51239
- })
51240
- ]);
51241
- if (timeoutHandle !== null)
51242
- clearTimeout(timeoutHandle);
51564
+ }
51565
+ if (extended.length === 0)
51566
+ return;
51567
+ emitProgress("running");
51568
+ await delay(Math.min(GRACE_INTERVAL_MS, Math.max(1000, stallMs)));
51569
+ }
51570
+ })().catch(() => {});
51571
+ await Promise.race([allDone, deadlineWatcher]);
51572
+ if (!settled)
51573
+ await Promise.race([allDone, delay(DRAIN_TIMEOUT_MS)]);
51243
51574
  clearInterval(progressHandle);
51244
51575
  emitProgress("settled");
51245
51576
  process.off("SIGINT", sigintHandler);
@@ -51425,14 +51756,19 @@ function formatVerdict(verdict, sessionPath) {
51425
51756
  }
51426
51757
  return output;
51427
51758
  }
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;
51759
+ 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) => {
51760
+ const t = setTimeout(resolve4, ms);
51761
+ t.unref?.();
51762
+ }), BENIGN_STDERR_PATTERNS, SENTINEL_MODELS;
51429
51763
  var init_team_orchestrator = __esm(() => {
51430
51764
  init_prehydrate();
51765
+ init_process_tree();
51431
51766
  init_redact();
51432
51767
  init_team_stats();
51433
51768
  init_team_stream_capture();
51434
51769
  API_ERROR_RE = /\[API Error:\s*([^\]]{0,300})\]/i;
51435
51770
  BG_CEILING_RE = /Background tasks still running after (\d+)s; terminating/i;
51771
+ BENIGN_STDERR_PATTERNS = [/^\s*\[claude-code:unrecognized_model\]/];
51436
51772
  SENTINEL_MODELS = new Set([
51437
51773
  "internal",
51438
51774
  "default",
@@ -52976,6 +53312,11 @@ function findMagmuxBinary() {
52976
53312
  throw new Error(`magmux not found. Install it:
52977
53313
  brew install MadAppGang/tap/magmux`);
52978
53314
  }
53315
+ function withoutControlPanes(evt) {
53316
+ if (!Array.isArray(evt.panes))
53317
+ return evt;
53318
+ return { ...evt, panes: evt.panes.filter((p) => p?.control !== true) };
53319
+ }
52979
53320
  async function subscribeToMagmux(sockPath, onEvent) {
52980
53321
  let client = null;
52981
53322
  for (let attempt = 0;attempt < 40; attempt++) {
@@ -53012,7 +53353,7 @@ async function subscribeToMagmux(sockPath, onEvent) {
53012
53353
  const evt = JSON.parse(line);
53013
53354
  onEvent?.(evt);
53014
53355
  if (evt.type === "results") {
53015
- finalResults = evt;
53356
+ finalResults = withoutControlPanes(evt);
53016
53357
  }
53017
53358
  } catch {}
53018
53359
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudish",
3
- "version": "7.51.0",
3
+ "version": "7.52.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.52.0",
64
+ "@claudish/magmux-darwin-x64": "7.52.0",
65
+ "@claudish/magmux-linux-arm64": "7.52.0",
66
+ "@claudish/magmux-linux-x64": "7.52.0"
67
67
  },
68
68
  "author": "Jack Rudenko <i@madappgang.com>",
69
69
  "license": "MIT",