@tractorscorch/clank 1.4.6 → 1.4.8

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/CHANGELOG.md CHANGED
@@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [1.4.8] — 2026-03-22
10
+
11
+ ### Fixed
12
+ - **Model hangs permanently after tool calls** — provider timeout was bypassed when the engine passed its own AbortSignal (always); now uses `AbortSignal.any()` to combine the caller's signal with a hard 120s timeout so hung models are detected and reported instead of blocking forever
13
+ - **No retry on timeout** — engine no longer retries when a model times out (was doubling the wait to 240s with no chance of success); timeouts propagate immediately as errors
14
+
15
+ ---
16
+
17
+ ## [1.4.7] — 2026-03-22
18
+
19
+ ### Fixed
20
+ - **Tool calling crashes gateway** — context compaction could split tool call / tool result message pairs, sending orphaned messages to Ollama which returns 400 errors and corrupts the session permanently; compaction now drops complete pairs together
21
+ - **Orphaned tool result safety net** — Ollama provider now filters out orphaned tool results before sending to the API, preventing 400 errors even if compaction misses a pair
22
+
23
+ ---
24
+
9
25
  ## [1.4.6] — 2026-03-22
10
26
 
11
27
  ### Fixed
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <a href="https://github.com/ItsTrag1c/Clank/releases/latest"><img src="https://img.shields.io/badge/version-1.4.6-blue.svg" alt="Version" /></a>
12
+ <a href="https://github.com/ItsTrag1c/Clank/releases/latest"><img src="https://img.shields.io/badge/version-1.4.8-blue.svg" alt="Version" /></a>
13
13
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License" /></a>
14
14
  <a href="https://www.npmjs.com/package/@tractorscorch/clank"><img src="https://img.shields.io/npm/v/@tractorscorch/clank.svg" alt="npm" /></a>
15
15
  <a href="https://github.com/ItsTrag1c/Clank/stargazers"><img src="https://img.shields.io/github/stars/ItsTrag1c/Clank.svg" alt="Stars" /></a>
@@ -75,7 +75,7 @@ That's it. Setup auto-detects your local models, configures the gateway, and get
75
75
  | Platform | Download |
76
76
  |----------|----------|
77
77
  | **npm** (all platforms) | `npm install -g @tractorscorch/clank` |
78
- | **macOS** (Apple Silicon) | [Clank_1.4.6_macos](https://github.com/ItsTrag1c/Clank/releases/latest/download/Clank_1.4.6_macos) |
78
+ | **macOS** (Apple Silicon) | [Clank_1.4.8_macos](https://github.com/ItsTrag1c/Clank/releases/latest/download/Clank_1.4.8_macos) |
79
79
 
80
80
  ## Features
81
81
 
package/dist/index.js CHANGED
@@ -216,6 +216,8 @@ var init_context_engine = __esm({
216
216
  }
217
217
  /**
218
218
  * Tier 1 aggressive: drop oldest messages when regular compaction isn't enough.
219
+ * Drops tool call + tool result pairs together to avoid orphaned messages
220
+ * that cause API errors (Ollama/OpenAI require matching pairs).
219
221
  */
220
222
  compactTier1Aggressive() {
221
223
  const protectedCount = 6;
@@ -223,7 +225,38 @@ var init_context_engine = __esm({
223
225
  while (this.estimateTokens() > budget.conversation * 0.7 && this.messages.length > protectedCount + 2) {
224
226
  const dropIdx = this.messages.findIndex((m) => m.role !== "system");
225
227
  if (dropIdx === -1 || dropIdx >= this.messages.length - protectedCount) break;
226
- this.messages.splice(dropIdx, 1);
228
+ const msg = this.messages[dropIdx];
229
+ if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
230
+ const toolCallIds = new Set(msg.tool_calls.map((tc) => tc.id));
231
+ this.messages.splice(dropIdx, 1);
232
+ for (let i = dropIdx; i < this.messages.length; ) {
233
+ if (this.messages[i].role === "tool" && toolCallIds.has(this.messages[i].tool_call_id || "")) {
234
+ this.messages.splice(i, 1);
235
+ } else {
236
+ break;
237
+ }
238
+ }
239
+ } else if (msg.role === "tool" && msg.tool_call_id) {
240
+ const parentIdx = this.messages.findLastIndex(
241
+ (m, idx) => idx < dropIdx && m.role === "assistant" && m.tool_calls?.some((tc) => tc.id === msg.tool_call_id)
242
+ );
243
+ if (parentIdx >= 0) {
244
+ const parent = this.messages[parentIdx];
245
+ const parentToolIds = new Set(parent.tool_calls.map((tc) => tc.id));
246
+ this.messages.splice(parentIdx, 1);
247
+ for (let i = parentIdx; i < this.messages.length; ) {
248
+ if (this.messages[i].role === "tool" && parentToolIds.has(this.messages[i].tool_call_id || "")) {
249
+ this.messages.splice(i, 1);
250
+ } else {
251
+ break;
252
+ }
253
+ }
254
+ } else {
255
+ this.messages.splice(dropIdx, 1);
256
+ }
257
+ } else {
258
+ this.messages.splice(dropIdx, 1);
259
+ }
227
260
  }
228
261
  }
229
262
  /**
@@ -238,7 +271,10 @@ var init_context_engine = __esm({
238
271
  if (!this.provider) return;
239
272
  const protectedCount = 6;
240
273
  if (this.messages.length <= protectedCount + 2) return;
241
- const cutoff = Math.max(2, this.messages.length - protectedCount - 2);
274
+ let cutoff = Math.max(2, this.messages.length - protectedCount - 2);
275
+ while (cutoff < this.messages.length && this.messages[cutoff].role === "tool") {
276
+ cutoff++;
277
+ }
242
278
  const toSummarize = this.messages.slice(0, cutoff);
243
279
  const toKeep = this.messages.slice(cutoff);
244
280
  const conversationText = toSummarize.map((m) => {
@@ -478,11 +514,23 @@ var init_ollama = __esm({
478
514
  }));
479
515
  }
480
516
  async *stream(messages, systemPrompt, tools, signal) {
517
+ const toolCallIds = /* @__PURE__ */ new Set();
518
+ for (const msg of messages) {
519
+ if (msg.role === "assistant" && msg.tool_calls) {
520
+ for (const tc of msg.tool_calls) toolCallIds.add(tc.id);
521
+ }
522
+ }
523
+ const sanitized = messages.filter((msg) => {
524
+ if (msg.role === "tool" && msg.tool_call_id && !toolCallIds.has(msg.tool_call_id)) {
525
+ return false;
526
+ }
527
+ return true;
528
+ });
481
529
  const apiMessages = [];
482
530
  if (systemPrompt) {
483
531
  apiMessages.push({ role: "system", content: systemPrompt });
484
532
  }
485
- for (const msg of messages) {
533
+ for (const msg of sanitized) {
486
534
  if (msg.role === "tool") {
487
535
  apiMessages.push({
488
536
  role: "tool",
@@ -513,7 +561,8 @@ var init_ollama = __esm({
513
561
  if (this.maxResponseTokens) {
514
562
  body.max_tokens = this.maxResponseTokens;
515
563
  }
516
- const effectiveSignal = signal || AbortSignal.timeout(12e4);
564
+ const timeoutSignal = AbortSignal.timeout(12e4);
565
+ const effectiveSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
517
566
  const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
518
567
  method: "POST",
519
568
  headers: { "Content-Type": "application/json" },
@@ -899,7 +948,8 @@ var init_agent = __esm({
899
948
  streamSuccess = true;
900
949
  break;
901
950
  } catch (streamErr) {
902
- if (attempt === 0 && !signal.aborted) {
951
+ const isTimeout = streamErr instanceof Error && (streamErr.name === "TimeoutError" || streamErr.name === "AbortError" || streamErr.message.includes("timed out"));
952
+ if (attempt === 0 && !signal.aborted && !isTimeout) {
903
953
  this.emit("error", {
904
954
  message: `Model connection failed, retrying... (${streamErr instanceof Error ? streamErr.message : "unknown"})`,
905
955
  recoverable: true
@@ -2709,7 +2759,8 @@ var init_anthropic = __esm({
2709
2759
  if (tools.length > 0) {
2710
2760
  body.tools = this.formatTools(tools);
2711
2761
  }
2712
- const effectiveSignal = signal || AbortSignal.timeout(9e4);
2762
+ const timeoutSignal = AbortSignal.timeout(9e4);
2763
+ const effectiveSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
2713
2764
  const res = await fetch(`${this.baseUrl}/v1/messages`, {
2714
2765
  method: "POST",
2715
2766
  headers: {
@@ -2936,7 +2987,8 @@ var init_openai = __esm({
2936
2987
  if (this.apiKey) {
2937
2988
  headers["Authorization"] = `Bearer ${this.apiKey}`;
2938
2989
  }
2939
- const effectiveSignal = signal || AbortSignal.timeout(9e4);
2990
+ const timeoutSignal = AbortSignal.timeout(9e4);
2991
+ const effectiveSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
2940
2992
  const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
2941
2993
  method: "POST",
2942
2994
  headers,
@@ -3163,7 +3215,8 @@ var init_google = __esm({
3163
3215
  body.tools = this.formatTools(tools);
3164
3216
  }
3165
3217
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:streamGenerateContent?key=${this.apiKey}&alt=sse`;
3166
- const effectiveSignal = signal || AbortSignal.timeout(9e4);
3218
+ const timeoutSignal = AbortSignal.timeout(9e4);
3219
+ const effectiveSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
3167
3220
  const res = await fetch(url, {
3168
3221
  method: "POST",
3169
3222
  headers: { "Content-Type": "application/json" },
@@ -6078,7 +6131,7 @@ var init_server = __esm({
6078
6131
  res.writeHead(200, { "Content-Type": "application/json" });
6079
6132
  res.end(JSON.stringify({
6080
6133
  status: "ok",
6081
- version: "1.4.6",
6134
+ version: "1.4.8",
6082
6135
  uptime: process.uptime(),
6083
6136
  clients: this.clients.size,
6084
6137
  agents: this.engines.size
@@ -6190,7 +6243,7 @@ var init_server = __esm({
6190
6243
  const hello = {
6191
6244
  type: "hello",
6192
6245
  protocol: PROTOCOL_VERSION,
6193
- version: "1.4.6",
6246
+ version: "1.4.8",
6194
6247
  agents: this.config.agents.list.map((a) => ({
6195
6248
  id: a.id,
6196
6249
  name: a.name || a.id,
@@ -7584,7 +7637,7 @@ async function runTui(opts) {
7584
7637
  ws.on("open", () => {
7585
7638
  ws.send(JSON.stringify({
7586
7639
  type: "connect",
7587
- params: { auth: { token }, mode: "tui", version: "1.4.6" }
7640
+ params: { auth: { token }, mode: "tui", version: "1.4.8" }
7588
7641
  }));
7589
7642
  });
7590
7643
  ws.on("message", (data) => {
@@ -8013,7 +8066,7 @@ import { fileURLToPath as fileURLToPath5 } from "url";
8013
8066
  import { dirname as dirname5, join as join19 } from "path";
8014
8067
  var __filename3 = fileURLToPath5(import.meta.url);
8015
8068
  var __dirname3 = dirname5(__filename3);
8016
- var version = "1.4.6";
8069
+ var version = "1.4.8";
8017
8070
  try {
8018
8071
  const pkg = JSON.parse(readFileSync(join19(__dirname3, "..", "package.json"), "utf-8"));
8019
8072
  version = pkg.version;