opencode-auto-resume 1.0.15 → 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 +110 -13
  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,
@@ -12414,7 +12415,9 @@ var DONE_CLAIM_PATTERNS = [
12414
12415
  /^finished[.!]*$/im,
12415
12416
  /^complete[.!]*$/im,
12416
12417
  /^task\s+complete[.!]*$/im,
12418
+ /^task\s+completed[.!]*$/im,
12417
12419
  /^all\s+tasks?\s+complete[.!]*$/im,
12420
+ /^all\s+tasks?\s+completed[.!]*$/im,
12418
12421
  /^(?:i['']?m\s+)?done\s+with\s+task/im
12419
12422
  ];
12420
12423
  var DONE_WITHOUT_WORK_PROMPT = "I need you to verify more carefully that you have actually completed all the required tasks. " + "Your response indicated you're done, but no work was detected. Please check your todo list " + "and complete any remaining work.";
@@ -12459,9 +12462,7 @@ var AutoResumePlugin = async (ctx, options) => {
12459
12462
  }
12460
12463
  async function log(level, msg) {
12461
12464
  try {
12462
- await ctx.client.app.log({
12463
- body: { service: "auto-resume", level, message: msg }
12464
- });
12465
+ await ctx.client.app.log({ body: { service: "auto-resume", level, message: msg } });
12465
12466
  } catch {}
12466
12467
  }
12467
12468
  function ensureWatch(sid) {
@@ -12486,7 +12487,9 @@ var AutoResumePlugin = async (ctx, options) => {
12486
12487
  toolTextTimer: null,
12487
12488
  checkingToolText: false,
12488
12489
  lastSubagentCheckAt: 0,
12489
- interruptedContinueCount: 0
12490
+ interruptedContinueCount: 0,
12491
+ recentToolCalls: [],
12492
+ toolLoopAttempts: 0
12490
12493
  };
12491
12494
  sessions.set(sid, w);
12492
12495
  }
@@ -12729,6 +12732,8 @@ var AutoResumePlugin = async (ctx, options) => {
12729
12732
  w.todoCheckAttempts = 0;
12730
12733
  w.checkingToolText = false;
12731
12734
  w.interruptedContinueCount = 0;
12735
+ w.recentToolCalls = [];
12736
+ w.toolLoopAttempts = 0;
12732
12737
  if (w.toolTextTimer) {
12733
12738
  clearTimeout(w.toolTextTimer);
12734
12739
  w.toolTextTimer = null;
@@ -12740,6 +12745,41 @@ var AutoResumePlugin = async (ctx, options) => {
12740
12745
  w.orphanWatchStartAt = null;
12741
12746
  w.idleSince = Date.now();
12742
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
+ }
12743
12783
  async function checkForToolCallAsText(sid, w) {
12744
12784
  if (typeof sid !== "string" || !sid)
12745
12785
  return;
@@ -12771,6 +12811,24 @@ var AutoResumePlugin = async (ctx, options) => {
12771
12811
  const rawRole = msg.role ?? msg.info?.role;
12772
12812
  if (rawRole !== "assistant")
12773
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
+ }
12774
12832
  const parts = msg.parts;
12775
12833
  if (!parts)
12776
12834
  continue;
@@ -12786,21 +12844,34 @@ var AutoResumePlugin = async (ctx, options) => {
12786
12844
  isReasoning = true;
12787
12845
  } else if (partType === "tool_use") {
12788
12846
  isToolUse = true;
12789
- const toolName = part.name ?? "unknown";
12790
- text = `tool_use: ${toolName}`;
12847
+ const toolName2 = part.name ?? "unknown";
12848
+ text = `tool_use: ${toolName2}`;
12791
12849
  } else {
12792
12850
  continue;
12793
12851
  }
12794
12852
  allAssistantText += text + `
12795
12853
  `;
12796
12854
  if (isToolUse) {
12797
- const candidate = {
12798
- prompt: "continue",
12799
- source: "tool-use",
12800
- priority: 1
12801
- };
12802
- if (!bestCandidate || candidate.priority < bestCandidate.priority) {
12803
- 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
+ }
12804
12875
  }
12805
12876
  }
12806
12877
  if (containsToolCallAsText(text)) {
@@ -12842,6 +12913,18 @@ var AutoResumePlugin = async (ctx, options) => {
12842
12913
  bestCandidate = candidate;
12843
12914
  }
12844
12915
  }
12916
+ if (!bestCandidate && containsDoneClaimPattern(text)) {
12917
+ const todos = w.todos || [];
12918
+ const hasOpenTodos = todos.some((t) => t.status === "pending" || t.status === "in_progress");
12919
+ if (hasOpenTodos) {
12920
+ await log("info", `${short(sid)} - model claims done but todos remain open. Sending recovery prompt...`);
12921
+ bestCandidate = {
12922
+ prompt: DONE_WITHOUT_WORK_PROMPT,
12923
+ source: "done-claim-no-emoji",
12924
+ priority: 1
12925
+ };
12926
+ }
12927
+ }
12845
12928
  }
12846
12929
  }
12847
12930
  const trimmedText = allAssistantText.trim();
@@ -12855,6 +12938,20 @@ var AutoResumePlugin = async (ctx, options) => {
12855
12938
  }
12856
12939
  return;
12857
12940
  }
12941
+ if (!bestCandidate) {
12942
+ const todos = w.todos || [];
12943
+ const hasOpenTodos = todos.some((t) => t.status === "pending" || t.status === "in_progress");
12944
+ if (hasOpenTodos && busyCount() === 1) {
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...`);
12946
+ bestCandidate = {
12947
+ prompt: "continue",
12948
+ source: "idle-with-open-todos",
12949
+ priority: 2
12950
+ };
12951
+ } else if (hasOpenTodos && busyCount() > 1) {
12952
+ await log("debug", `${short(sid)} - todos remain open but ${busyCount()} sessions busy (subagents running), skipping continue`);
12953
+ }
12954
+ }
12858
12955
  if (!bestCandidate)
12859
12956
  return;
12860
12957
  w.toolTextRecovered = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-auto-resume",
3
- "version": "1.0.15",
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",