@tractorscorch/clank 1.7.2 → 1.7.3

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,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [1.7.3] — 2026-03-25
10
+
11
+ ### Added
12
+ - **Smart memory for local models** — local models no longer load the full MEMORY.md into the system prompt (wastes context tokens). Instead, memories are injected via TF-IDF relevance matching with a tighter 800-char budget, keeping only what's relevant to the current conversation
13
+ - **System file protection rule** — agents are now instructed not to modify, delete, or overwrite files outside the workspace or current working directory unless the user explicitly names the file. Prevents local models from touching system files, OS directories, or config dotfiles unprompted
14
+ - **Proactive auto-compaction** — context is now checked after every tool result (not just at the start of each loop iteration). If a large tool result pushes context past the threshold, tier-1 compaction fires immediately with zero latency. A pre-send safety check at 90% utilization catches anything that slips through
15
+ - **Context overflow recovery** — if a provider returns a context-length error, the engine now auto-compacts aggressively and retries once instead of crashing. If context is still critically full (>95%), warns the user to use `/compact` or `/new`
16
+
17
+ ### Fixed
18
+ - **TF-IDF memory relevance** — `buildMemoryBlock()` was called with an empty query string, making relevance scoring useless. Now passes session context for proper matching
19
+
20
+ ---
21
+
9
22
  ## [1.7.2] — 2026-03-24
10
23
 
11
24
  ### Added
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.7.2-blue.svg" alt="Version" /></a>
12
+ <a href="https://github.com/ItsTrag1c/Clank/releases/latest"><img src="https://img.shields.io/badge/version-1.7.3-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>
@@ -92,7 +92,7 @@ That's it. Setup auto-detects your local models, configures the gateway, and get
92
92
  | Platform | Download |
93
93
  |----------|----------|
94
94
  | **npm** (all platforms) | `npm install -g @tractorscorch/clank` |
95
- | **macOS** (Apple Silicon) | [Clank_1.7.2_macos](https://github.com/ItsTrag1c/Clank/releases/latest/download/Clank_1.7.2_macos) |
95
+ | **macOS** (Apple Silicon) | [Clank_1.7.3_macos](https://github.com/ItsTrag1c/Clank/releases/latest/download/Clank_1.7.3_macos) |
96
96
 
97
97
  ## Security Notice
98
98
 
package/dist/index.js CHANGED
@@ -320,6 +320,30 @@ ${summary.trim()}`,
320
320
  this.compactTier1Aggressive();
321
321
  }
322
322
  }
323
+ /** Check if context is critically full (>95% utilization) */
324
+ isOverflowing() {
325
+ return this.utilizationPercent() >= 95;
326
+ }
327
+ /**
328
+ * Tier-1 only compaction — synchronous, no LLM call.
329
+ * Safe to call mid-turn without adding latency.
330
+ */
331
+ compactTier1Only() {
332
+ const before = this.messages.length;
333
+ const tokensBefore = this.estimateTokens();
334
+ this.compactTier1();
335
+ if (this.utilizationPercent() >= 70) {
336
+ this.compactTier1Aggressive();
337
+ }
338
+ return {
339
+ ok: true,
340
+ tier: 1,
341
+ messagesBefore: before,
342
+ messagesAfter: this.messages.length,
343
+ estimatedTokensBefore: tokensBefore,
344
+ estimatedTokensAfter: this.estimateTokens()
345
+ };
346
+ }
323
347
  /** Clear all messages */
324
348
  clear() {
325
349
  this.messages = [];
@@ -988,6 +1012,9 @@ ${results}`
988
1012
  outputTokens = 0;
989
1013
  }
990
1014
  try {
1015
+ if (this.contextEngine.utilizationPercent() > 90) {
1016
+ this.contextEngine.compactTier1Only();
1017
+ }
991
1018
  const streamIterator = activeProvider.stream(
992
1019
  this.contextEngine.getMessages(),
993
1020
  this.systemPrompt,
@@ -1024,6 +1051,21 @@ ${results}`
1024
1051
  } catch (streamErr) {
1025
1052
  const errMsg = streamErr instanceof Error ? streamErr.message : "unknown";
1026
1053
  const isTimeout = streamErr instanceof Error && (streamErr.name === "TimeoutError" || streamErr.name === "AbortError" || errMsg.includes("timed out"));
1054
+ const isContextError = /context.*(length|limit|exceeded)|too many tokens|maximum.*tokens|token limit/i.test(errMsg);
1055
+ if (attempt === 0 && isContextError && !signal.aborted) {
1056
+ this.emit("error", {
1057
+ message: "Context limit hit \u2014 compacting and retrying...",
1058
+ recoverable: true
1059
+ });
1060
+ this.contextEngine.compactTier1Only();
1061
+ if (this.contextEngine.isOverflowing()) {
1062
+ this.emit("error", {
1063
+ message: "Context is nearly full \u2014 use /compact or /new to free space",
1064
+ recoverable: true
1065
+ });
1066
+ }
1067
+ continue;
1068
+ }
1027
1069
  const isRetryable = !isTimeout && !signal.aborted && (errMsg.includes("connection dropped") || errMsg.includes("stopped responding") || errMsg.includes("empty response") || errMsg.includes("fetch failed") || errMsg.includes("ECONNREFUSED") || errMsg.includes("ECONNRESET"));
1028
1070
  if (attempt === 0 && (isRetryable || !signal.aborted && !isTimeout)) {
1029
1071
  this.emit("error", {
@@ -1129,6 +1171,15 @@ ${results}`
1129
1171
  this.contextEngine.ingest(result);
1130
1172
  this.emit("tool-result", { id: tc.id, name: tc.name, success: false, summary: errMsg });
1131
1173
  }
1174
+ if (this.contextEngine.needsCompaction()) {
1175
+ this.contextEngine.compactTier1Only();
1176
+ if (this.contextEngine.isOverflowing()) {
1177
+ this.emit("error", {
1178
+ message: "Context is nearly full \u2014 use /compact or /new to free space",
1179
+ recoverable: true
1180
+ });
1181
+ }
1182
+ }
1132
1183
  }
1133
1184
  }
1134
1185
  if (iterationCount >= MAX_ITERATIONS) {
@@ -1265,9 +1316,11 @@ import { platform, hostname } from "os";
1265
1316
  async function buildSystemPrompt(opts) {
1266
1317
  const parts = [];
1267
1318
  const compact = opts.compact ?? false;
1319
+ const isLocal = opts.isLocal ?? false;
1268
1320
  const isSubAgent = (opts.spawnDepth ?? 0) > 0;
1269
1321
  if (!compact) {
1270
- const workspaceContent = await loadWorkspaceFiles(opts.workspaceDir);
1322
+ const files = isLocal ? WORKSPACE_FILES_LOCAL : WORKSPACE_FILES;
1323
+ const workspaceContent = await loadWorkspaceFiles(opts.workspaceDir, files);
1271
1324
  if (workspaceContent) {
1272
1325
  parts.push(workspaceContent);
1273
1326
  parts.push("---");
@@ -1297,7 +1350,8 @@ async function buildSystemPrompt(opts) {
1297
1350
  "You are an AI agent running LOCALLY on the user's machine.",
1298
1351
  "You have tools: read_file, write_file, edit_file, list_directory, bash, search_files, glob_files, git, web_search, web_fetch, and self-config tools.",
1299
1352
  "ALWAYS use your tools. NEVER say you cannot access files, run commands, or perform actions. You CAN \u2014 use your tools.",
1300
- "NEVER apologize and refuse. If asked to do something, DO IT with your tools or explain what tool you need."
1353
+ "NEVER apologize and refuse. If asked to do something, DO IT with your tools or explain what tool you need.",
1354
+ "Do NOT modify files outside your workspace or the user's current directory unless the user explicitly names the file."
1301
1355
  ].join(" "));
1302
1356
  } else {
1303
1357
  parts.push("## CRITICAL: You Are a Local Agent With Tools");
@@ -1313,13 +1367,18 @@ async function buildSystemPrompt(opts) {
1313
1367
  parts.push("3. NEVER apologize and refuse to act. If a task requires a tool, use it. If you lack a specific tool, say which tool you need \u2014 do not give a generic refusal.");
1314
1368
  parts.push("4. Read files before editing them. Use tools proactively without being asked twice.");
1315
1369
  parts.push("5. You can configure yourself \u2014 use the config, channel, agent, and model management tools to modify your own setup.");
1370
+ parts.push("6. Do NOT modify, delete, or overwrite files outside your workspace directory or the user's current working directory unless the user explicitly names the file. System files, OS directories, and config dotfiles are off-limits by default.");
1316
1371
  }
1317
1372
  if (opts.thinking === "off") {
1318
1373
  parts.push("");
1319
1374
  parts.push("Do NOT use extended thinking or reasoning blocks. Respond directly and concisely.");
1320
1375
  }
1321
1376
  parts.push("");
1322
- parts.push("When you learn something important about the user or project, save it using the config or memory tools so you remember it next time.");
1377
+ if (isLocal) {
1378
+ parts.push("Your memories are managed automatically. Use memory tools to save or recall important information. Do not rely on conversation history for long-term facts.");
1379
+ } else {
1380
+ parts.push("When you learn something important about the user or project, save it using the config or memory tools so you remember it next time.");
1381
+ }
1323
1382
  parts.push("");
1324
1383
  const projectMemory = await loadProjectMemory(opts.identity.workspace);
1325
1384
  if (projectMemory) {
@@ -1329,9 +1388,9 @@ async function buildSystemPrompt(opts) {
1329
1388
  }
1330
1389
  return parts.join("\n");
1331
1390
  }
1332
- async function loadWorkspaceFiles(workspaceDir) {
1391
+ async function loadWorkspaceFiles(workspaceDir, files = WORKSPACE_FILES) {
1333
1392
  const sections = [];
1334
- for (const filename of WORKSPACE_FILES) {
1393
+ for (const filename of files) {
1335
1394
  const filePath = join2(workspaceDir, filename);
1336
1395
  if (existsSync2(filePath)) {
1337
1396
  try {
@@ -1383,7 +1442,7 @@ async function ensureWorkspaceFiles(workspaceDir, templateDir) {
1383
1442
  }
1384
1443
  }
1385
1444
  }
1386
- var WORKSPACE_FILES, SUB_AGENT_FILE;
1445
+ var WORKSPACE_FILES, WORKSPACE_FILES_LOCAL, SUB_AGENT_FILE;
1387
1446
  var init_system_prompt = __esm({
1388
1447
  "src/engine/system-prompt.ts"() {
1389
1448
  "use strict";
@@ -1396,6 +1455,13 @@ var init_system_prompt = __esm({
1396
1455
  "TOOLS.md",
1397
1456
  "MEMORY.md"
1398
1457
  ];
1458
+ WORKSPACE_FILES_LOCAL = [
1459
+ "SOUL.md",
1460
+ "USER.md",
1461
+ "IDENTITY.md",
1462
+ "AGENTS.md",
1463
+ "TOOLS.md"
1464
+ ];
1399
1465
  SUB_AGENT_FILE = "RUNNER.md";
1400
1466
  }
1401
1467
  });
@@ -5150,7 +5216,7 @@ async function runChat(opts) {
5150
5216
  console.log(cyan(" / __|| | __ _ _ _ | |__"));
5151
5217
  console.log(cyan(" | (__ | |/ _` || ' \\| / /"));
5152
5218
  console.log(cyan(" \\___||_|\\__,_||_||_|_\\_\\"));
5153
- console.log(dim(` v1.7.2 | ${resolved.modelId} | ${identity.toolTier} tier`));
5219
+ console.log(dim(` v1.7.3 | ${resolved.modelId} | ${identity.toolTier} tier`));
5154
5220
  console.log(dim(" Type your message. Press Ctrl+C to exit.\n"));
5155
5221
  const rl = createInterface({
5156
5222
  input: process.stdin,
@@ -6817,7 +6883,7 @@ _Kill with /kill <id> or /killall_`;
6817
6883
  return !current ? "\u{1F4AD} Thinking display *on* \u2014 you'll see the model's reasoning above responses." : "\u{1F4AD} Thinking display *off* \u2014 only the final response will be shown.";
6818
6884
  }
6819
6885
  case "version": {
6820
- return `\u{1F527} *Clank* v1.7.2`;
6886
+ return `\u{1F527} *Clank* v1.7.3`;
6821
6887
  }
6822
6888
  default:
6823
6889
  return null;
@@ -7407,7 +7473,7 @@ var init_server = __esm({
7407
7473
  res.writeHead(200, { "Content-Type": "application/json" });
7408
7474
  res.end(JSON.stringify({
7409
7475
  status: "ok",
7410
- version: "1.7.2",
7476
+ version: "1.7.3",
7411
7477
  uptime: process.uptime(),
7412
7478
  clients: this.clients.size,
7413
7479
  agents: this.engines.size
@@ -7519,7 +7585,7 @@ var init_server = __esm({
7519
7585
  const hello = {
7520
7586
  type: "hello",
7521
7587
  protocol: PROTOCOL_VERSION,
7522
- version: "1.7.2",
7588
+ version: "1.7.3",
7523
7589
  agents: this.config.agents.list.map((a) => ({
7524
7590
  id: a.id,
7525
7591
  name: a.name || a.id,
@@ -7829,10 +7895,11 @@ var init_server = __esm({
7829
7895
  channel,
7830
7896
  compact,
7831
7897
  thinking,
7832
- spawnDepth: currentDepth
7898
+ spawnDepth: currentDepth,
7899
+ isLocal: resolved.isLocal
7833
7900
  });
7834
- const memoryBudget = resolved.isLocal ? 1500 : 4e3;
7835
- const memoryBlock = await this.memoryManager.buildMemoryBlock("", identity.workspace, memoryBudget);
7901
+ const memoryBudget = resolved.isLocal ? 800 : 4e3;
7902
+ const memoryBlock = await this.memoryManager.buildMemoryBlock("session", identity.workspace, memoryBudget);
7836
7903
  const fullPrompt = memoryBlock ? systemPrompt + "\n\n---\n\n" + memoryBlock : systemPrompt;
7837
7904
  const maxSpawnDepth = this.config.agents.defaults.subagents?.maxSpawnDepth ?? 1;
7838
7905
  const maxConcurrent = this.config.agents.defaults.subagents?.maxConcurrent ?? 8;
@@ -9149,7 +9216,7 @@ async function runTui(opts) {
9149
9216
  ws.on("open", () => {
9150
9217
  ws.send(JSON.stringify({
9151
9218
  type: "connect",
9152
- params: { auth: { token }, mode: "tui", version: "1.7.2" }
9219
+ params: { auth: { token }, mode: "tui", version: "1.7.3" }
9153
9220
  }));
9154
9221
  });
9155
9222
  ws.on("message", (data) => {
@@ -9592,7 +9659,7 @@ import { fileURLToPath as fileURLToPath5 } from "url";
9592
9659
  import { dirname as dirname5, join as join20 } from "path";
9593
9660
  var __filename3 = fileURLToPath5(import.meta.url);
9594
9661
  var __dirname3 = dirname5(__filename3);
9595
- var version = "1.7.2";
9662
+ var version = "1.7.3";
9596
9663
  try {
9597
9664
  const pkg = JSON.parse(readFileSync(join20(__dirname3, "..", "package.json"), "utf-8"));
9598
9665
  version = pkg.version;