@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 +16 -0
- package/README.md +2 -2
- package/dist/index.js +65 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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;
|