coder-agent 2.9.9 → 2.9.11

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/dist/agent.js CHANGED
@@ -248,11 +248,10 @@ function getLoopCheckKey(toolCalls) {
248
248
  const end = args.end_line ?? "";
249
249
  return `read_file_lines:${normalizedPath}:${start}-${end}`;
250
250
  case "write_file":
251
- return `write_file:${normalizedPath}:${normalizeCode(args.content || "")}`;
251
+ return `write_file:${normalizedPath}`;
252
252
  case "patch_file":
253
253
  const targetCode = normalizeCode(args.target_code || "");
254
- const replacementCode = normalizeCode(args.replacement_code || "");
255
- return `patch_file:${normalizedPath}:${targetCode}:${replacementCode}`;
254
+ return `patch_file:${normalizedPath}:${targetCode}`;
256
255
  case "list_directory":
257
256
  return `list_directory:${normalizedPath}`;
258
257
  case "run_shell":
@@ -277,7 +276,7 @@ function hasRepeatingCycle(history) {
277
276
  const n = history.length;
278
277
  for (let len = 1; len <= 4; len++) {
279
278
  if (n >= len * 2) {
280
- const minRepeats = len === 1 ? 3 : 2;
279
+ const minRepeats = len === 1 ? 2 : (len === 2 ? 3 : 2);
281
280
  if (n >= len * minRepeats) {
282
281
  let isLoop = true;
283
282
  const lastBlock = history.slice(n - len);
@@ -296,6 +295,59 @@ function hasRepeatingCycle(history) {
296
295
  }
297
296
  return false;
298
297
  }
298
+ function getJaccardSimilarity(str1, str2) {
299
+ const getWords = (str) => {
300
+ return new Set(str
301
+ .toLowerCase()
302
+ .replace(/[^a-z0-9\s]/g, "")
303
+ .split(/\s+/)
304
+ .filter(w => w.length > 2));
305
+ };
306
+ const words1 = getWords(str1);
307
+ const words2 = getWords(str2);
308
+ if (words1.size === 0 && words2.size === 0)
309
+ return 1.0;
310
+ if (words1.size === 0 || words2.size === 0)
311
+ return 0.0;
312
+ let intersectionCount = 0;
313
+ for (const w of words1) {
314
+ if (words2.has(w)) {
315
+ intersectionCount++;
316
+ }
317
+ }
318
+ const unionSize = words1.size + words2.size - intersectionCount;
319
+ return intersectionCount / unionSize;
320
+ }
321
+ function hasRepeatingThoughtCycle(thoughts) {
322
+ const n = thoughts.length;
323
+ const nonEmptyThoughts = thoughts.filter(t => t.trim().length > 10);
324
+ const m = nonEmptyThoughts.length;
325
+ for (let len = 1; len <= 4; len++) {
326
+ if (m >= len * 2) {
327
+ const minRepeats = len === 1 ? 2 : (len === 2 ? 3 : 2);
328
+ if (m >= len * minRepeats) {
329
+ let isLoop = true;
330
+ const lastBlock = nonEmptyThoughts.slice(m - len);
331
+ for (let r = 1; r < minRepeats; r++) {
332
+ const prevBlock = nonEmptyThoughts.slice(m - len * (r + 1), m - len * r);
333
+ for (let i = 0; i < len; i++) {
334
+ const sim = getJaccardSimilarity(lastBlock[i], prevBlock[i]);
335
+ if (sim < 0.60) {
336
+ isLoop = false;
337
+ break;
338
+ }
339
+ }
340
+ if (!isLoop)
341
+ break;
342
+ }
343
+ if (isLoop) {
344
+ return true;
345
+ }
346
+ }
347
+ }
348
+ }
349
+ return false;
350
+ }
299
351
  // ─── Extract Text Tool Calls ──────────────────────────────────────────────────
300
352
  function extractTextToolCalls(content) {
301
353
  const calls = [];
@@ -757,6 +809,7 @@ export class Agent {
757
809
  const modifiedFiles = new Set();
758
810
  let cleanContent = "";
759
811
  const stateHistory = [];
812
+ const thoughtHistory = [];
760
813
  while (true) {
761
814
  if (signal?.aborted) {
762
815
  const abortErr = new Error("The user aborted a request.");
@@ -921,22 +974,35 @@ export class Agent {
921
974
  // Loop detection & intervention
922
975
  const currentKey = getLoopCheckKey(toolCalls);
923
976
  const tempHistory = [...stateHistory, currentKey];
924
- if (hasRepeatingCycle(tempHistory)) {
977
+ const currentThought = msg.content || "";
978
+ const tempThoughtHistory = [...thoughtHistory, currentThought];
979
+ let loopTriggered = false;
980
+ if (hasRepeatingCycle(tempHistory) || hasRepeatingThoughtCycle(tempThoughtHistory)) {
981
+ loopTriggered = true;
982
+ }
983
+ if (loopTriggered) {
925
984
  loopInterventions++;
926
985
  if (loopInterventions >= 2) {
927
986
  console.log(chalk.hex('#ff453a')('\n✕ Loop intervention failed: Coder is stuck in an execution loop. Exiting to prompt.'));
928
987
  break;
929
988
  }
930
- const warningMessage = `⚠️ [LOOP DETECTED] You are repeating the exact same thoughts or tool calls. Please break out of this loop. Do not repeat the same actions. Re-evaluate your strategy, look at a different file, run a different command, or ask the user for clarification if you cannot proceed.`;
931
- console.log(chalk.hex('#ff9f0a')('\n⚠ Loop detected! Intervening to break the loop...'));
932
- this.memory.add({
933
- role: "user",
934
- content: warningMessage,
935
- });
989
+ const originalGoal = this.memory.getAll().find(m => m.role === "user")?.content || userMessage;
990
+ const warningMessage = `⚠️ [LOOP DETECTED & CONTEXT RESET] You got stuck in a repeating thinking/execution loop. To break the loop, all intermediate repetitive chat context has been discarded.
991
+
992
+ Here is your original goal:
993
+ "${originalGoal}"
994
+
995
+ Please start fresh. Re-evaluate your strategy, check other files, run different commands, or ask the user for clarification directly. Do NOT repeat the same failed tool calls.`;
996
+ console.log(chalk.hex('#ff9f0a')('\n⚠ Loop detected! Compressing memory and resetting context window...'));
997
+ this.memory.resetToInitialPrompt(warningMessage);
936
998
  stateHistory.length = 0; // Reset history to allow a fresh start
999
+ thoughtHistory.length = 0;
937
1000
  }
938
1001
  else {
939
1002
  stateHistory.push(currentKey);
1003
+ if (currentThought.trim().length > 10) {
1004
+ thoughtHistory.push(currentThought);
1005
+ }
940
1006
  if (loopInterventions > 0)
941
1007
  loopInterventions = 0;
942
1008
  }
package/dist/memory.js CHANGED
@@ -524,6 +524,25 @@ export class Memory {
524
524
  this.messages = [this.messages[0]]; // keep system prompt
525
525
  console.log(" Memory cleared.");
526
526
  }
527
+ resetToInitialPrompt(warningMessage) {
528
+ if (this.messages.length <= 1)
529
+ return;
530
+ const systemMsg = this.messages[0];
531
+ const firstUserMsg = this.messages.find(m => m.role === "user");
532
+ if (firstUserMsg) {
533
+ this.messages = [
534
+ systemMsg,
535
+ firstUserMsg,
536
+ { role: "user", content: warningMessage }
537
+ ];
538
+ }
539
+ else {
540
+ this.messages = [
541
+ systemMsg,
542
+ { role: "user", content: warningMessage }
543
+ ];
544
+ }
545
+ }
527
546
  summary() {
528
547
  const turns = this.messages.filter(m => m.role === "user").length;
529
548
  return `${turns} turn(s) in memory (${getMemoryScopeDisplay(this.scope)} scope)`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coder-agent",
3
- "version": "2.9.9",
3
+ "version": "2.9.11",
4
4
  "description": "CLI coding agent powered by Google Gemini",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",