opencode-auto-resume 1.0.16 → 1.0.17

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/index.js +85 -14
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12351,6 +12351,7 @@ var IDLE_CLEANUP_MS = 10 * 60000;
12351
12351
  var SESSION_DISCOVERY_INTERVAL_MS = 60000;
12352
12352
  var TOOL_TEXT_RECOVERY_PROMPT = "Your last message contained a raw tool call printed as text instead of being executed. " + "Please use the proper tool calling mechanism to execute it.";
12353
12353
  var THINKING_TOOL_RECOVERY_PROMPT = "I noticed you have a tool call generated in your thinking/reasoning. " + "Please execute it using the proper tool calling mechanism instead of keeping it in reasoning.";
12354
+ var TOOL_LOOP_RECOVERY_PROMPT = "I notice you've been calling the same tool multiple times in a row without making progress. " + "Please step back and reassess your approach. Consider: " + "1) Are you stuck in a loop? 2) Do you need different information first? " + "3) Should you try a different tool or break the task into smaller steps? " + "Take a moment to think about what's blocking you and propose a different strategy.";
12354
12355
  var TOOL_TEXT_PATTERNS = [
12355
12356
  /<function\s*=/i,
12356
12357
  /<function>/i,
@@ -12461,9 +12462,7 @@ var AutoResumePlugin = async (ctx, options) => {
12461
12462
  }
12462
12463
  async function log(level, msg) {
12463
12464
  try {
12464
- await ctx.client.app.log({
12465
- body: { service: "auto-resume", level, message: msg }
12466
- });
12465
+ await ctx.client.app.log({ body: { service: "auto-resume", level, message: msg } });
12467
12466
  } catch {}
12468
12467
  }
12469
12468
  function ensureWatch(sid) {
@@ -12488,7 +12487,9 @@ var AutoResumePlugin = async (ctx, options) => {
12488
12487
  toolTextTimer: null,
12489
12488
  checkingToolText: false,
12490
12489
  lastSubagentCheckAt: 0,
12491
- interruptedContinueCount: 0
12490
+ interruptedContinueCount: 0,
12491
+ recentToolCalls: [],
12492
+ toolLoopAttempts: 0
12492
12493
  };
12493
12494
  sessions.set(sid, w);
12494
12495
  }
@@ -12731,6 +12732,8 @@ var AutoResumePlugin = async (ctx, options) => {
12731
12732
  w.todoCheckAttempts = 0;
12732
12733
  w.checkingToolText = false;
12733
12734
  w.interruptedContinueCount = 0;
12735
+ w.recentToolCalls = [];
12736
+ w.toolLoopAttempts = 0;
12734
12737
  if (w.toolTextTimer) {
12735
12738
  clearTimeout(w.toolTextTimer);
12736
12739
  w.toolTextTimer = null;
@@ -12742,6 +12745,41 @@ var AutoResumePlugin = async (ctx, options) => {
12742
12745
  w.orphanWatchStartAt = null;
12743
12746
  w.idleSince = Date.now();
12744
12747
  }
12748
+ function detectPatternLoop(recentTools) {
12749
+ if (recentTools.length < 6)
12750
+ return false;
12751
+ for (const patternLen of [2, 3, 4, 5]) {
12752
+ if (recentTools.length < patternLen * 3)
12753
+ continue;
12754
+ const pattern = recentTools.slice(-patternLen);
12755
+ let matches = 0;
12756
+ for (let i = recentTools.length - patternLen * 2;i >= 0; i -= patternLen) {
12757
+ const slice = recentTools.slice(i, i + patternLen);
12758
+ if (slice.length !== patternLen)
12759
+ break;
12760
+ const isMatch = slice.every((tool3, idx) => tool3 === pattern[idx]);
12761
+ if (!isMatch)
12762
+ break;
12763
+ matches++;
12764
+ }
12765
+ if (matches >= 2)
12766
+ return true;
12767
+ }
12768
+ return false;
12769
+ }
12770
+ function trackToolCall(w, toolName2) {
12771
+ const now = Date.now();
12772
+ w.recentToolCalls = w.recentToolCalls.filter((call) => now - call.at < 120000);
12773
+ w.recentToolCalls.push({ toolName: toolName2, at: now });
12774
+ const recentTools = w.recentToolCalls.slice(-15).map((call) => call.toolName);
12775
+ if (recentTools.length < 6)
12776
+ return false;
12777
+ const lastTool = recentTools[recentTools.length - 1];
12778
+ const consecutiveSame = recentTools.slice(-5).filter((tool3) => tool3 === lastTool).length;
12779
+ if (consecutiveSame >= 3)
12780
+ return true;
12781
+ return detectPatternLoop(recentTools);
12782
+ }
12745
12783
  async function checkForToolCallAsText(sid, w) {
12746
12784
  if (typeof sid !== "string" || !sid)
12747
12785
  return;
@@ -12773,6 +12811,24 @@ var AutoResumePlugin = async (ctx, options) => {
12773
12811
  const rawRole = msg.role ?? msg.info?.role;
12774
12812
  if (rawRole !== "assistant")
12775
12813
  continue;
12814
+ const toolCall = msg.toolCall;
12815
+ if (toolCall && typeof toolCall === "object" && "name" in toolCall) {
12816
+ const toolName2 = toolCall.name;
12817
+ if (toolName2) {
12818
+ trackToolCall(w, toolName2);
12819
+ }
12820
+ }
12821
+ const toolCalls = msg.tool_calls;
12822
+ if (toolCalls) {
12823
+ for (const tc of toolCalls) {
12824
+ if (typeof tc === "object" && "name" in tc) {
12825
+ const toolName2 = tc.name;
12826
+ if (toolName2) {
12827
+ trackToolCall(w, toolName2);
12828
+ }
12829
+ }
12830
+ }
12831
+ }
12776
12832
  const parts = msg.parts;
12777
12833
  if (!parts)
12778
12834
  continue;
@@ -12788,21 +12844,34 @@ var AutoResumePlugin = async (ctx, options) => {
12788
12844
  isReasoning = true;
12789
12845
  } else if (partType === "tool_use") {
12790
12846
  isToolUse = true;
12791
- const toolName = part.name ?? "unknown";
12792
- text = `tool_use: ${toolName}`;
12847
+ const toolName2 = part.name ?? "unknown";
12848
+ text = `tool_use: ${toolName2}`;
12793
12849
  } else {
12794
12850
  continue;
12795
12851
  }
12796
12852
  allAssistantText += text + `
12797
12853
  `;
12798
12854
  if (isToolUse) {
12799
- const candidate = {
12800
- prompt: "continue",
12801
- source: "tool-use",
12802
- priority: 1
12803
- };
12804
- if (!bestCandidate || candidate.priority < bestCandidate.priority) {
12805
- bestCandidate = candidate;
12855
+ const isLoop = trackToolCall(w, toolName);
12856
+ if (isLoop && w.toolLoopAttempts < 2) {
12857
+ w.toolLoopAttempts++;
12858
+ const candidate = {
12859
+ prompt: TOOL_LOOP_RECOVERY_PROMPT,
12860
+ source: "tool-loop",
12861
+ priority: 0
12862
+ };
12863
+ if (!bestCandidate || candidate.priority < bestCandidate.priority) {
12864
+ bestCandidate = candidate;
12865
+ }
12866
+ } else {
12867
+ const candidate = {
12868
+ prompt: "continue",
12869
+ source: "tool-use",
12870
+ priority: 1
12871
+ };
12872
+ if (!bestCandidate || candidate.priority < bestCandidate.priority) {
12873
+ bestCandidate = candidate;
12874
+ }
12806
12875
  }
12807
12876
  }
12808
12877
  if (containsToolCallAsText(text)) {
@@ -12872,13 +12941,15 @@ var AutoResumePlugin = async (ctx, options) => {
12872
12941
  if (!bestCandidate) {
12873
12942
  const todos = w.todos || [];
12874
12943
  const hasOpenTodos = todos.some((t) => t.status === "pending" || t.status === "in_progress");
12875
- if (hasOpenTodos) {
12944
+ if (hasOpenTodos && busyCount() === 1) {
12876
12945
  await log("info", `${short(sid)} - no activity detected but todos remain open (${todos.filter((t) => t.status === "pending" || t.status === "in_progress").length} tasks). Sending continue...`);
12877
12946
  bestCandidate = {
12878
12947
  prompt: "continue",
12879
12948
  source: "idle-with-open-todos",
12880
12949
  priority: 2
12881
12950
  };
12951
+ } else if (hasOpenTodos && busyCount() > 1) {
12952
+ await log("debug", `${short(sid)} - todos remain open but ${busyCount()} sessions busy (subagents running), skipping continue`);
12882
12953
  }
12883
12954
  }
12884
12955
  if (!bestCandidate)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-auto-resume",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "description": "OpenCode plugin that automatically resumes stalled LLM sessions when thinking/streaming freezes mid-generation.",
5
5
  "keywords": [
6
6
  "opencode",