opencode-auto-resume 1.0.16 → 1.0.18

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 +105 -20
  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,10 @@ 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,
12493
+ isSubagent: false
12492
12494
  };
12493
12495
  sessions.set(sid, w);
12494
12496
  }
@@ -12723,7 +12725,9 @@ var AutoResumePlugin = async (ctx, options) => {
12723
12725
  w.gaveUp = false;
12724
12726
  w.orphanWatchStartAt = null;
12725
12727
  w.aborting = false;
12726
- w.toolTextRecovered = false;
12728
+ if (w.isSubagent) {
12729
+ w.toolTextRecovered = false;
12730
+ }
12727
12731
  w.toolTextAttempts = 0;
12728
12732
  w.continueTimestamps = [];
12729
12733
  w.idleSince = null;
@@ -12731,6 +12735,8 @@ var AutoResumePlugin = async (ctx, options) => {
12731
12735
  w.todoCheckAttempts = 0;
12732
12736
  w.checkingToolText = false;
12733
12737
  w.interruptedContinueCount = 0;
12738
+ w.recentToolCalls = [];
12739
+ w.toolLoopAttempts = 0;
12734
12740
  if (w.toolTextTimer) {
12735
12741
  clearTimeout(w.toolTextTimer);
12736
12742
  w.toolTextTimer = null;
@@ -12742,6 +12748,41 @@ var AutoResumePlugin = async (ctx, options) => {
12742
12748
  w.orphanWatchStartAt = null;
12743
12749
  w.idleSince = Date.now();
12744
12750
  }
12751
+ function detectPatternLoop(recentTools) {
12752
+ if (recentTools.length < 6)
12753
+ return false;
12754
+ for (const patternLen of [2, 3, 4, 5]) {
12755
+ if (recentTools.length < patternLen * 3)
12756
+ continue;
12757
+ const pattern = recentTools.slice(-patternLen);
12758
+ let matches = 0;
12759
+ for (let i = recentTools.length - patternLen * 2;i >= 0; i -= patternLen) {
12760
+ const slice = recentTools.slice(i, i + patternLen);
12761
+ if (slice.length !== patternLen)
12762
+ break;
12763
+ const isMatch = slice.every((tool3, idx) => tool3 === pattern[idx]);
12764
+ if (!isMatch)
12765
+ break;
12766
+ matches++;
12767
+ }
12768
+ if (matches >= 2)
12769
+ return true;
12770
+ }
12771
+ return false;
12772
+ }
12773
+ function trackToolCall(w, toolName2) {
12774
+ const now = Date.now();
12775
+ w.recentToolCalls = w.recentToolCalls.filter((call) => now - call.at < 120000);
12776
+ w.recentToolCalls.push({ toolName: toolName2, at: now });
12777
+ const recentTools = w.recentToolCalls.slice(-15).map((call) => call.toolName);
12778
+ if (recentTools.length < 6)
12779
+ return false;
12780
+ const lastTool = recentTools[recentTools.length - 1];
12781
+ const consecutiveSame = recentTools.slice(-5).filter((tool3) => tool3 === lastTool).length;
12782
+ if (consecutiveSame >= 3)
12783
+ return true;
12784
+ return detectPatternLoop(recentTools);
12785
+ }
12745
12786
  async function checkForToolCallAsText(sid, w) {
12746
12787
  if (typeof sid !== "string" || !sid)
12747
12788
  return;
@@ -12773,6 +12814,24 @@ var AutoResumePlugin = async (ctx, options) => {
12773
12814
  const rawRole = msg.role ?? msg.info?.role;
12774
12815
  if (rawRole !== "assistant")
12775
12816
  continue;
12817
+ const toolCall = msg.toolCall;
12818
+ if (toolCall && typeof toolCall === "object" && "name" in toolCall) {
12819
+ const toolName2 = toolCall.name;
12820
+ if (toolName2) {
12821
+ trackToolCall(w, toolName2);
12822
+ }
12823
+ }
12824
+ const toolCalls = msg.tool_calls;
12825
+ if (toolCalls) {
12826
+ for (const tc of toolCalls) {
12827
+ if (typeof tc === "object" && "name" in tc) {
12828
+ const toolName2 = tc.name;
12829
+ if (toolName2) {
12830
+ trackToolCall(w, toolName2);
12831
+ }
12832
+ }
12833
+ }
12834
+ }
12776
12835
  const parts = msg.parts;
12777
12836
  if (!parts)
12778
12837
  continue;
@@ -12788,21 +12847,34 @@ var AutoResumePlugin = async (ctx, options) => {
12788
12847
  isReasoning = true;
12789
12848
  } else if (partType === "tool_use") {
12790
12849
  isToolUse = true;
12791
- const toolName = part.name ?? "unknown";
12792
- text = `tool_use: ${toolName}`;
12850
+ const toolName2 = part.name ?? "unknown";
12851
+ text = `tool_use: ${toolName2}`;
12793
12852
  } else {
12794
12853
  continue;
12795
12854
  }
12796
12855
  allAssistantText += text + `
12797
12856
  `;
12798
12857
  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;
12858
+ const isLoop = trackToolCall(w, toolName);
12859
+ if (isLoop && w.toolLoopAttempts < 2) {
12860
+ w.toolLoopAttempts++;
12861
+ const candidate = {
12862
+ prompt: TOOL_LOOP_RECOVERY_PROMPT,
12863
+ source: "tool-loop",
12864
+ priority: 0
12865
+ };
12866
+ if (!bestCandidate || candidate.priority < bestCandidate.priority) {
12867
+ bestCandidate = candidate;
12868
+ }
12869
+ } else {
12870
+ const candidate = {
12871
+ prompt: "continue",
12872
+ source: "tool-use",
12873
+ priority: 1
12874
+ };
12875
+ if (!bestCandidate || candidate.priority < bestCandidate.priority) {
12876
+ bestCandidate = candidate;
12877
+ }
12806
12878
  }
12807
12879
  }
12808
12880
  if (containsToolCallAsText(text)) {
@@ -12872,13 +12944,15 @@ var AutoResumePlugin = async (ctx, options) => {
12872
12944
  if (!bestCandidate) {
12873
12945
  const todos = w.todos || [];
12874
12946
  const hasOpenTodos = todos.some((t) => t.status === "pending" || t.status === "in_progress");
12875
- if (hasOpenTodos) {
12947
+ if (hasOpenTodos && busyCount() === 1) {
12876
12948
  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
12949
  bestCandidate = {
12878
12950
  prompt: "continue",
12879
12951
  source: "idle-with-open-todos",
12880
12952
  priority: 2
12881
12953
  };
12954
+ } else if (hasOpenTodos && busyCount() > 1) {
12955
+ await log("debug", `${short(sid)} - todos remain open but ${busyCount()} sessions busy (subagents running), skipping continue`);
12882
12956
  }
12883
12957
  }
12884
12958
  if (!bestCandidate)
@@ -13111,11 +13185,20 @@ var AutoResumePlugin = async (ctx, options) => {
13111
13185
  const lone = getLoneBusySession();
13112
13186
  if (lone && lone.w.orphanWatchStartAt === null) {
13113
13187
  lone.w.orphanWatchStartAt = Date.now();
13188
+ w.isSubagent = true;
13114
13189
  log("info", `Subagent finished, parent ${short(lone.sid)} stuck. Orphan watch (${subagentWaitMs / 1000}s).`);
13115
13190
  }
13116
13191
  }
13117
13192
  prevBusyCount = currentBusy;
13118
13193
  log("debug", `${short(sid)} -> idle (${currentBusy})${statusType === "interrupted" ? " (interrupted)" : ""}`);
13194
+ if (!w.isSubagent) {
13195
+ const todos = w.todos || [];
13196
+ const hasOpenTodos = todos.some((t) => t.status === "pending" || t.status === "in_progress");
13197
+ if (hasOpenTodos && currentBusy === 1 && !w.toolTextRecovered) {
13198
+ await log("info", `${short(sid)} - idle with ${todos.filter((t) => t.status === "pending" || t.status === "in_progress").length} open todos. Sending continue...`);
13199
+ tryResume(sid, w, "Idle with open todos");
13200
+ }
13201
+ }
13119
13202
  if (!w.toolTextRecovered && w.toolTextAttempts < maxRetries) {
13120
13203
  if (w.toolTextTimer)
13121
13204
  clearTimeout(w.toolTextTimer);
@@ -13244,12 +13327,14 @@ var AutoResumePlugin = async (ctx, options) => {
13244
13327
  execute: async (_args, ctx2) => {
13245
13328
  const w = sessions.get(ctx2.sessionID);
13246
13329
  if (w) {
13247
- w.toolTextRecovered = true;
13248
- if (w.toolTextTimer) {
13249
- clearTimeout(w.toolTextTimer);
13250
- w.toolTextTimer = null;
13330
+ if (!w.isSubagent) {
13331
+ w.toolTextRecovered = true;
13332
+ if (w.toolTextTimer) {
13333
+ clearTimeout(w.toolTextTimer);
13334
+ w.toolTextTimer = null;
13335
+ }
13251
13336
  }
13252
- log("info", `${short(ctx2.sessionID)} - task_complete called, agent done`);
13337
+ log("info", `${short(ctx2.sessionID)} - task_complete called, ${w.isSubagent ? "subagent" : "agent"} done`);
13253
13338
  }
13254
13339
  return "Task completion acknowledged. No further continuation will be sent.";
13255
13340
  }
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.18",
4
4
  "description": "OpenCode plugin that automatically resumes stalled LLM sessions when thinking/streaming freezes mid-generation.",
5
5
  "keywords": [
6
6
  "opencode",