coder-agent 2.7.1 → 2.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/dist/agent.js CHANGED
@@ -180,6 +180,62 @@ function formatResponseText(text) {
180
180
  return chalk.white(part);
181
181
  }).join('');
182
182
  }
183
+ function normalizeContentForLoopCheck(content) {
184
+ if (!content)
185
+ return "";
186
+ return content
187
+ .toLowerCase()
188
+ .replace(/\s+/g, " ")
189
+ .trim();
190
+ }
191
+ function normalizeToolCallsForLoopCheck(toolCalls) {
192
+ if (!toolCalls || toolCalls.length === 0)
193
+ return "";
194
+ return toolCalls.map(tc => {
195
+ const name = tc.function?.name || "";
196
+ let argsStr = "";
197
+ try {
198
+ const rawArgs = tc.function?.arguments;
199
+ const args = typeof rawArgs === 'string'
200
+ ? JSON.parse(rawArgs)
201
+ : rawArgs;
202
+ if (args && typeof args === 'object') {
203
+ const sortedArgs = {};
204
+ for (const key of Object.keys(args).sort()) {
205
+ sortedArgs[key] = args[key];
206
+ }
207
+ argsStr = JSON.stringify(sortedArgs);
208
+ }
209
+ }
210
+ catch {
211
+ argsStr = tc.function?.arguments || "";
212
+ }
213
+ return `${name}(${argsStr})`;
214
+ }).join(";");
215
+ }
216
+ function hasRepeatingCycle(history) {
217
+ const n = history.length;
218
+ for (let len = 1; len <= 4; len++) {
219
+ if (n >= len * 2) {
220
+ const minRepeats = len === 1 ? 3 : 2;
221
+ if (n >= len * minRepeats) {
222
+ let isLoop = true;
223
+ const lastBlock = history.slice(n - len);
224
+ for (let r = 1; r < minRepeats; r++) {
225
+ const prevBlock = history.slice(n - len * (r + 1), n - len * r);
226
+ if (JSON.stringify(lastBlock) !== JSON.stringify(prevBlock)) {
227
+ isLoop = false;
228
+ break;
229
+ }
230
+ }
231
+ if (isLoop) {
232
+ return true;
233
+ }
234
+ }
235
+ }
236
+ }
237
+ return false;
238
+ }
183
239
  // ─── Extract Text Tool Calls ──────────────────────────────────────────────────
184
240
  function extractTextToolCalls(content) {
185
241
  const calls = [];
@@ -568,6 +624,7 @@ export class Agent {
568
624
  const MAX_WAITS = 3;
569
625
  const modifiedFiles = new Set();
570
626
  let cleanContent = "";
627
+ const stateHistory = [];
571
628
  while (true) {
572
629
  if (signal?.aborted) {
573
630
  const abortErr = new Error("The user aborted a request.");
@@ -707,6 +764,21 @@ export class Agent {
707
764
  for (const line of statusLines) {
708
765
  console.log(line);
709
766
  }
767
+ // Loop detection & intervention
768
+ const currentKey = `${normalizeContentForLoopCheck(msg.content || "")}|${normalizeToolCallsForLoopCheck(toolCalls)}`;
769
+ const tempHistory = [...stateHistory, currentKey];
770
+ if (hasRepeatingCycle(tempHistory)) {
771
+ 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.`;
772
+ console.log(chalk.hex('#ff9f0a')('\n⚠ Loop detected! Intervening to break the loop...'));
773
+ this.memory.add({
774
+ role: "user",
775
+ content: warningMessage,
776
+ });
777
+ stateHistory.length = 0; // Reset history to allow a fresh start
778
+ }
779
+ else {
780
+ stateHistory.push(currentKey);
781
+ }
710
782
  if (iterations < MAX_ITERATIONS) {
711
783
  console.log(chalk.dim('\n' + '─'.repeat(48) + '\n'));
712
784
  startSpinner("thinking...");
package/dist/index.js CHANGED
@@ -183,10 +183,52 @@ async function main() {
183
183
  if (!apiKey) {
184
184
  apiKey = await promptApiKey();
185
185
  }
186
+ let currentAbortController = null;
187
+ let originalListeners = [];
188
+ let isHijacked = false;
189
+ let tempSigintHandler = null;
190
+ const startHijack = () => {
191
+ if (isHijacked)
192
+ return;
193
+ originalListeners = process.stdin.listeners("data");
194
+ for (const listener of originalListeners) {
195
+ process.stdin.removeListener("data", listener);
196
+ }
197
+ tempSigintHandler = (data) => {
198
+ if (data.includes(3)) { // Ctrl+C byte
199
+ if (currentAbortController) {
200
+ currentAbortController.abort();
201
+ }
202
+ }
203
+ };
204
+ process.stdin.on("data", tempSigintHandler);
205
+ process.stdin.resume();
206
+ isHijacked = true;
207
+ };
208
+ const stopHijack = () => {
209
+ if (!isHijacked)
210
+ return;
211
+ process.stdin.removeListener("data", tempSigintHandler);
212
+ for (const listener of originalListeners) {
213
+ process.stdin.on("data", listener);
214
+ }
215
+ originalListeners = [];
216
+ tempSigintHandler = null;
217
+ isHijacked = false;
218
+ };
186
219
  const confirmHandler = async (question) => {
187
220
  if (rl) {
221
+ const wasHijacked = isHijacked;
222
+ if (wasHijacked) {
223
+ stopHijack();
224
+ rl.resume();
225
+ }
188
226
  return new Promise((resolve) => {
189
227
  rl.question(question, (answer) => {
228
+ if (wasHijacked) {
229
+ rl.pause();
230
+ startHijack();
231
+ }
190
232
  resolve(answer.trim().toLowerCase().startsWith("y"));
191
233
  });
192
234
  });
@@ -236,7 +278,6 @@ async function main() {
236
278
  let inputBuffer = "";
237
279
  let pasteTimeout = null;
238
280
  let lineCountInBurst = 0;
239
- let currentAbortController = null;
240
281
  async function executeAgentChat(trimmed) {
241
282
  // Pause standard input processing during agent thinking & updates
242
283
  rl.pause();
@@ -294,20 +335,7 @@ async function main() {
294
335
  return;
295
336
  }
296
337
  currentAbortController = new AbortController();
297
- // Hijack stdin data listeners during agent execution to allow Ctrl+C to abort the agent immediately
298
- const originalListeners = process.stdin.listeners("data");
299
- for (const listener of originalListeners) {
300
- process.stdin.removeListener("data", listener);
301
- }
302
- const tempSigintHandler = (data) => {
303
- if (data.includes(3)) { // Ctrl+C byte
304
- if (currentAbortController) {
305
- currentAbortController.abort();
306
- }
307
- }
308
- };
309
- process.stdin.on("data", tempSigintHandler);
310
- process.stdin.resume();
338
+ startHijack();
311
339
  try {
312
340
  await agent.chat(trimmed, currentAbortController.signal);
313
341
  }
@@ -329,10 +357,7 @@ async function main() {
329
357
  }
330
358
  }
331
359
  finally {
332
- process.stdin.removeListener("data", tempSigintHandler);
333
- for (const listener of originalListeners) {
334
- process.stdin.on("data", listener);
335
- }
360
+ stopHijack();
336
361
  currentAbortController = null;
337
362
  }
338
363
  rl.resume();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coder-agent",
3
- "version": "2.7.1",
3
+ "version": "2.7.3",
4
4
  "description": "CLI coding agent powered by Google Gemini",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",