@tractorscorch/clank 1.4.3 → 1.4.5

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,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [1.4.5] — 2026-03-22
10
+
11
+ ### Fixed
12
+ - **Gateway unresponsive after messages** — WebSocket frame handler was not awaited, causing unhandled promise rejections that silently killed the gateway process
13
+ - **Added `unhandledRejection` handler** — gateway now logs rejected promises instead of dying silently
14
+ - **Provider timeout fallback** — all providers (Ollama, Anthropic, OpenAI, Google) now have a fallback timeout (120s local, 90s cloud) if no abort signal is provided, preventing indefinite hangs when a model is unresponsive
15
+
16
+ ---
17
+
18
+ ## [1.4.4] — 2026-03-22
19
+
20
+ ### Fixed
21
+ - **Gateway crash after 4-5 messages** — confirmation handler WebSocket listeners were never removed on timeout, accumulating orphaned handlers per message until the process crashed
22
+ - **Engine listener limit** — set `maxListeners` to 30 on AgentEngine (Node.js default of 10 was too low since each message cycle wires 10 event listeners)
23
+ - **Rate limiter memory leak** — stale session entries in the rate limiter Map were never purged; added periodic cleanup when map exceeds 100 entries
24
+
25
+ ---
26
+
9
27
  ## [1.4.3] — 2026-03-22
10
28
 
11
29
  ### 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.3-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.5-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.3_macos](https://github.com/ItsTrag1c/Clank/releases/latest/download/Clank_1.4.3_macos) |
78
+ | **macOS** (Apple Silicon) | [Clank_1.4.5_macos](https://github.com/ItsTrag1c/Clank/releases/latest/download/Clank_1.4.5_macos) |
79
79
 
80
80
  ## Features
81
81
 
package/dist/index.js CHANGED
@@ -513,11 +513,12 @@ var init_ollama = __esm({
513
513
  if (this.maxResponseTokens) {
514
514
  body.max_tokens = this.maxResponseTokens;
515
515
  }
516
+ const effectiveSignal = signal || AbortSignal.timeout(12e4);
516
517
  const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
517
518
  method: "POST",
518
519
  headers: { "Content-Type": "application/json" },
519
520
  body: JSON.stringify(body),
520
- signal
521
+ signal: effectiveSignal
521
522
  });
522
523
  if (!res.ok) {
523
524
  const text = await res.text().catch(() => "Unknown error");
@@ -764,6 +765,7 @@ var init_agent = __esm({
764
765
  alwaysApproved = /* @__PURE__ */ new Set();
765
766
  constructor(opts) {
766
767
  super();
768
+ this.setMaxListeners(30);
767
769
  this.identity = opts.identity;
768
770
  this.toolRegistry = opts.toolRegistry;
769
771
  this.sessionStore = opts.sessionStore;
@@ -2707,6 +2709,7 @@ var init_anthropic = __esm({
2707
2709
  if (tools.length > 0) {
2708
2710
  body.tools = this.formatTools(tools);
2709
2711
  }
2712
+ const effectiveSignal = signal || AbortSignal.timeout(9e4);
2710
2713
  const res = await fetch(`${this.baseUrl}/v1/messages`, {
2711
2714
  method: "POST",
2712
2715
  headers: {
@@ -2715,7 +2718,7 @@ var init_anthropic = __esm({
2715
2718
  "anthropic-version": "2023-06-01"
2716
2719
  },
2717
2720
  body: JSON.stringify(body),
2718
- signal
2721
+ signal: effectiveSignal
2719
2722
  });
2720
2723
  if (!res.ok) {
2721
2724
  const text = await res.text().catch(() => "Unknown error");
@@ -2933,11 +2936,12 @@ var init_openai = __esm({
2933
2936
  if (this.apiKey) {
2934
2937
  headers["Authorization"] = `Bearer ${this.apiKey}`;
2935
2938
  }
2939
+ const effectiveSignal = signal || AbortSignal.timeout(9e4);
2936
2940
  const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
2937
2941
  method: "POST",
2938
2942
  headers,
2939
2943
  body: JSON.stringify(body),
2940
- signal
2944
+ signal: effectiveSignal
2941
2945
  });
2942
2946
  if (!res.ok) {
2943
2947
  const text = await res.text().catch(() => "Unknown error");
@@ -3159,11 +3163,12 @@ var init_google = __esm({
3159
3163
  body.tools = this.formatTools(tools);
3160
3164
  }
3161
3165
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:streamGenerateContent?key=${this.apiKey}&alt=sse`;
3166
+ const effectiveSignal = signal || AbortSignal.timeout(9e4);
3162
3167
  const res = await fetch(url, {
3163
3168
  method: "POST",
3164
3169
  headers: { "Content-Type": "application/json" },
3165
3170
  body: JSON.stringify(body),
3166
- signal
3171
+ signal: effectiveSignal
3167
3172
  });
3168
3173
  if (!res.ok) {
3169
3174
  const text = await res.text().catch(() => "Unknown error");
@@ -5892,6 +5897,9 @@ var init_server = __esm({
5892
5897
  }
5893
5898
  /** Start the gateway server */
5894
5899
  async start() {
5900
+ process.on("unhandledRejection", (err) => {
5901
+ console.error(` Unhandled rejection: ${err instanceof Error ? err.message : err}`);
5902
+ });
5895
5903
  if (this.config.gateway.auth.mode === "token" && !this.config.gateway.auth.token) {
5896
5904
  const { randomBytes: randomBytes2 } = await import("crypto");
5897
5905
  this.config.gateway.auth.token = randomBytes2(16).toString("hex");
@@ -6056,7 +6064,7 @@ var init_server = __esm({
6056
6064
  res.writeHead(200, { "Content-Type": "application/json" });
6057
6065
  res.end(JSON.stringify({
6058
6066
  status: "ok",
6059
- version: "1.4.3",
6067
+ version: "1.4.5",
6060
6068
  uptime: process.uptime(),
6061
6069
  clients: this.clients.size,
6062
6070
  agents: this.engines.size
@@ -6117,7 +6125,11 @@ var init_server = __esm({
6117
6125
  this.clients.set(ws, client);
6118
6126
  ws.on("message", (data) => {
6119
6127
  const frame = parseFrame(data.toString());
6120
- if (frame) this.handleFrame(client, frame);
6128
+ if (frame) {
6129
+ this.handleFrame(client, frame).catch((err) => {
6130
+ console.error(` Frame handler error: ${err instanceof Error ? err.message : err}`);
6131
+ });
6132
+ }
6121
6133
  });
6122
6134
  ws.on("close", () => {
6123
6135
  this.clients.delete(ws);
@@ -6164,7 +6176,7 @@ var init_server = __esm({
6164
6176
  const hello = {
6165
6177
  type: "hello",
6166
6178
  protocol: PROTOCOL_VERSION,
6167
- version: "1.4.3",
6179
+ version: "1.4.5",
6168
6180
  agents: this.config.agents.list.map((a) => ({
6169
6181
  id: a.id,
6170
6182
  name: a.name || a.id,
@@ -6363,6 +6375,13 @@ var init_server = __esm({
6363
6375
  const recent = timestamps.filter((t) => now - t < this.RATE_LIMIT_WINDOW);
6364
6376
  recent.push(now);
6365
6377
  this.rateLimiter.set(sessionKey, recent);
6378
+ if (this.rateLimiter.size > 100) {
6379
+ for (const [key, ts] of this.rateLimiter) {
6380
+ if (ts.length === 0 || now - ts[ts.length - 1] > this.RATE_LIMIT_WINDOW * 2) {
6381
+ this.rateLimiter.delete(key);
6382
+ }
6383
+ }
6384
+ }
6366
6385
  return recent.length > this.RATE_LIMIT_MAX;
6367
6386
  }
6368
6387
  /** Cancel current request for a client */
@@ -6448,7 +6467,6 @@ var init_server = __esm({
6448
6467
  }
6449
6468
  const confirmId = `confirm_${Date.now()}`;
6450
6469
  this.sendEvent(client, "confirm-needed", { id: confirmId, actions });
6451
- const timeout = setTimeout(() => resolve4(false), 3e4);
6452
6470
  const resolveHandler = (raw) => {
6453
6471
  const frame = parseFrame(raw.toString());
6454
6472
  if (frame?.type === "req" && frame.method === "confirm.resolve") {
@@ -6461,6 +6479,10 @@ var init_server = __esm({
6461
6479
  }
6462
6480
  };
6463
6481
  client.ws.on("message", resolveHandler);
6482
+ const timeout = setTimeout(() => {
6483
+ client.ws.removeListener("message", resolveHandler);
6484
+ resolve4(false);
6485
+ }, 3e4);
6464
6486
  };
6465
6487
  engine.on("confirm-needed", confirmListener);
6466
6488
  listeners.push(["confirm-needed", confirmListener]);
@@ -7548,7 +7570,7 @@ async function runTui(opts) {
7548
7570
  ws.on("open", () => {
7549
7571
  ws.send(JSON.stringify({
7550
7572
  type: "connect",
7551
- params: { auth: { token }, mode: "tui", version: "1.4.3" }
7573
+ params: { auth: { token }, mode: "tui", version: "1.4.5" }
7552
7574
  }));
7553
7575
  });
7554
7576
  ws.on("message", (data) => {
@@ -7977,7 +7999,7 @@ import { fileURLToPath as fileURLToPath5 } from "url";
7977
7999
  import { dirname as dirname5, join as join19 } from "path";
7978
8000
  var __filename3 = fileURLToPath5(import.meta.url);
7979
8001
  var __dirname3 = dirname5(__filename3);
7980
- var version = "1.4.3";
8002
+ var version = "1.4.5";
7981
8003
  try {
7982
8004
  const pkg = JSON.parse(readFileSync(join19(__dirname3, "..", "package.json"), "utf-8"));
7983
8005
  version = pkg.version;