cc-claw 0.12.7 → 0.12.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.
Files changed (2) hide show
  1. package/dist/cli.js +87 -5
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -72,7 +72,7 @@ var VERSION;
72
72
  var init_version = __esm({
73
73
  "src/version.ts"() {
74
74
  "use strict";
75
- VERSION = true ? "0.12.7" : (() => {
75
+ VERSION = true ? "0.12.8" : (() => {
76
76
  try {
77
77
  return JSON.parse(readFileSync(join2(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
78
78
  } catch {
@@ -4247,6 +4247,78 @@ var init_backends = __esm({
4247
4247
  }
4248
4248
  });
4249
4249
 
4250
+ // src/tool-loop-detector.ts
4251
+ function djb2Hash(str) {
4252
+ let hash = 5381;
4253
+ for (let i = 0; i < str.length; i++) {
4254
+ hash = (hash << 5) + hash + str.charCodeAt(i) & 4294967295;
4255
+ }
4256
+ return hash.toString(36);
4257
+ }
4258
+ function extractKeyField(input) {
4259
+ const key = input.file_path ?? input.path ?? input.file ?? input.command ?? input.cmd ?? input.url ?? input.query ?? input.search_query ?? input.pattern ?? input.content;
4260
+ if (key !== void 0 && key !== null) {
4261
+ return String(key);
4262
+ }
4263
+ const sorted = Object.keys(input).sort();
4264
+ const parts = sorted.map((k) => `${k}:${JSON.stringify(input[k])}`);
4265
+ return parts.join("|");
4266
+ }
4267
+ function fingerprint(toolName, input) {
4268
+ const key = extractKeyField(input);
4269
+ return `${toolName}:${djb2Hash(key)}`;
4270
+ }
4271
+ var DEFAULT_WINDOW_SIZE, DEFAULT_THRESHOLD, HARD_TURN_CAP, ToolLoopDetector;
4272
+ var init_tool_loop_detector = __esm({
4273
+ "src/tool-loop-detector.ts"() {
4274
+ "use strict";
4275
+ DEFAULT_WINDOW_SIZE = 15;
4276
+ DEFAULT_THRESHOLD = 3;
4277
+ HARD_TURN_CAP = 200;
4278
+ ToolLoopDetector = class {
4279
+ windowSize;
4280
+ threshold;
4281
+ window = [];
4282
+ constructor(windowSize = DEFAULT_WINDOW_SIZE, threshold = DEFAULT_THRESHOLD) {
4283
+ this.windowSize = windowSize;
4284
+ this.threshold = threshold;
4285
+ }
4286
+ /**
4287
+ * Record a tool call and check for loops.
4288
+ *
4289
+ * @returns `{ isLoop: true, reason }` if the same fingerprint appears
4290
+ * `threshold` or more times in the sliding window.
4291
+ */
4292
+ addCall(toolName, input) {
4293
+ const fp = fingerprint(toolName, input);
4294
+ this.window.push(fp);
4295
+ while (this.window.length > this.windowSize) {
4296
+ this.window.shift();
4297
+ }
4298
+ let count = 0;
4299
+ for (const entry of this.window) {
4300
+ if (entry === fp) count++;
4301
+ }
4302
+ if (count >= this.threshold) {
4303
+ return {
4304
+ isLoop: true,
4305
+ reason: `Tool "${toolName}" called ${count}\xD7 with same input in last ${this.window.length} calls`
4306
+ };
4307
+ }
4308
+ return { isLoop: false };
4309
+ }
4310
+ /** Number of calls tracked so far. */
4311
+ get callCount() {
4312
+ return this.window.length;
4313
+ }
4314
+ /** Reset the detector (e.g., after a retry with a fresh session). */
4315
+ reset() {
4316
+ this.window.length = 0;
4317
+ }
4318
+ };
4319
+ }
4320
+ });
4321
+
4250
4322
  // src/memory/inject.ts
4251
4323
  function daysSince(dateStr) {
4252
4324
  const date = /* @__PURE__ */ new Date(dateStr.replace(" ", "T") + (dateStr.includes("Z") ? "" : "Z"));
@@ -9170,6 +9242,7 @@ function spawnQuery(adapter, config2, model2, cancelState, thinkingLevel, timeou
9170
9242
  let sawToolEvents = false;
9171
9243
  let sawResultEvent = false;
9172
9244
  let toolTurnCount = 0;
9245
+ const loopDetector = new ToolLoopDetector();
9173
9246
  const t0 = Date.now();
9174
9247
  const elapsed = () => `${((Date.now() - t0) / 1e3).toFixed(1)}s`;
9175
9248
  const pendingTools = /* @__PURE__ */ new Map();
@@ -9215,10 +9288,19 @@ function spawnQuery(adapter, config2, model2, cancelState, thinkingLevel, timeou
9215
9288
  error("[agent] tool action error:", err);
9216
9289
  });
9217
9290
  }
9218
- if (maxTurns && adapter.id !== "claude") {
9291
+ if (adapter.id !== "claude") {
9219
9292
  toolTurnCount++;
9220
- if (toolTurnCount >= maxTurns) {
9221
- log(`[agent] Turn limit ${maxTurns} reached for ${adapter.id} -- stopping`);
9293
+ if (ev.toolName) {
9294
+ const check = loopDetector.addCall(ev.toolName, ev.toolInput ?? {});
9295
+ if (check.isLoop) {
9296
+ warn(`[agent] Loop detected for ${adapter.id}: ${check.reason} \u2014 stopping`);
9297
+ killProcessGroup(proc, "SIGTERM");
9298
+ }
9299
+ }
9300
+ const effectiveCap = maxTurns ?? HARD_TURN_CAP;
9301
+ if (toolTurnCount >= effectiveCap) {
9302
+ const label2 = maxTurns ? `Turn limit ${maxTurns}` : `Hard cap ${HARD_TURN_CAP}`;
9303
+ warn(`[agent] ${label2} reached for ${adapter.id} \u2014 stopping`);
9222
9304
  killProcessGroup(proc, "SIGTERM");
9223
9305
  }
9224
9306
  }
@@ -9523,6 +9605,7 @@ var activeChats, chatLocks, SPAWN_TIMEOUT_MS, MCP_CONFIG_FLAG;
9523
9605
  var init_agent = __esm({
9524
9606
  "src/agent.ts"() {
9525
9607
  "use strict";
9608
+ init_tool_loop_detector();
9526
9609
  init_store5();
9527
9610
  init_backends();
9528
9611
  init_paths();
@@ -14571,7 +14654,6 @@ async function handleSideQuest(parentChatId, msg, channel) {
14571
14654
  backend: backend2 ?? void 0,
14572
14655
  settingsSourceChatId: parentChatId,
14573
14656
  agentMode: "native",
14574
- maxTurns: 10,
14575
14657
  timeoutMs: 3e5,
14576
14658
  bootstrapTier: "full"
14577
14659
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-claw",
3
- "version": "0.12.7",
3
+ "version": "0.12.8",
4
4
  "description": "CC-Claw: Personal AI assistant on Telegram — multi-backend (Claude, Gemini, Codex, Cursor), sub-agent orchestration, MCP management",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",