opencode-auto-resume 1.1.9 → 1.1.10

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 +159 -39
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12439,13 +12439,17 @@ var DONE_CLAIM_PATTERNS = [
12439
12439
  /^task\s+completed[.!]*$/im,
12440
12440
  /^all\s+tasks?\s+complete[.!]*$/im,
12441
12441
  /^all\s+tasks?\s+completed[.!]*$/im,
12442
- /^(?:i['']?m\s+)?done\s+with\s+task/im
12442
+ /^(?:i['']?m\s+)?done\s+with\s+task/im,
12443
+ /\bdone\s+with\s+(?:the\s+)?(?:task|work|implementation)/im,
12444
+ /\bfinished\s+(?:the\s+)?(?:task|work|implementation)/im,
12445
+ /\b(?:all|everything)\s+(?:is\s+)?(?:complete|done|finished)/im,
12446
+ /\bnothing\s+(?:else\s+)?(?:left|remaining|to do)/im
12443
12447
  ];
12444
12448
  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.";
12445
12449
  function containsDoneClaimPattern(text) {
12446
12450
  const lines = text.split(`
12447
12451
  `);
12448
- const lastLines = lines.slice(-3).join(`
12452
+ const lastLines = lines.slice(-5).join(`
12449
12453
  `);
12450
12454
  return DONE_CLAIM_PATTERNS.some((pat) => pat.test(lastLines));
12451
12455
  }
@@ -12592,7 +12596,22 @@ var AutoResumePlugin = async (ctx, options) => {
12592
12596
  recordContinue(sid);
12593
12597
  return w.continueTimestamps.length >= loopMaxContinues;
12594
12598
  }
12599
+ const recentLogMsgs = new Map;
12600
+ const LOG_DEDUP_WINDOW_MS = 5000;
12595
12601
  async function log(level, msg) {
12602
+ if (level === "debug") {
12603
+ const key = `${level}:${msg}`;
12604
+ const now = Date.now();
12605
+ const last = recentLogMsgs.get(key);
12606
+ if (last && now - last < LOG_DEDUP_WINDOW_MS)
12607
+ return;
12608
+ recentLogMsgs.set(key, now);
12609
+ if (recentLogMsgs.size > 200) {
12610
+ const oldest = [...recentLogMsgs.entries()].sort((a, b) => a[1] - b[1]);
12611
+ for (let i = 0;i < 100; i++)
12612
+ recentLogMsgs.delete(oldest[i][0]);
12613
+ }
12614
+ }
12596
12615
  try {
12597
12616
  await ctx.client.app.log({ body: { service: "auto-resume", level, message: msg } });
12598
12617
  } catch (e) {
@@ -12782,6 +12801,8 @@ var AutoResumePlugin = async (ctx, options) => {
12782
12801
  await log("debug", `${short(sid)} - continue already in progress, skipping`);
12783
12802
  return;
12784
12803
  }
12804
+ if (w.userCancelled || w.completionSignaled)
12805
+ return;
12785
12806
  if (!w.continuing)
12786
12807
  dbg(`State transition on ${short(sid)}: continuing=false -> true`);
12787
12808
  w.continuing = true;
@@ -12917,9 +12938,21 @@ var AutoResumePlugin = async (ctx, options) => {
12917
12938
  return response.messages;
12918
12939
  return [];
12919
12940
  }
12941
+ const messagesInflight = new Map;
12920
12942
  async function getSessionMessages(sid) {
12921
- const response = await ctx.client.session.messages({ path: { id: sid } });
12922
- return extractMessages(response);
12943
+ const inflight = messagesInflight.get(sid);
12944
+ if (inflight)
12945
+ return inflight;
12946
+ const p = (async () => {
12947
+ try {
12948
+ const response = await ctx.client.session.messages({ path: { id: sid } });
12949
+ return extractMessages(response);
12950
+ } finally {
12951
+ messagesInflight.delete(sid);
12952
+ }
12953
+ })();
12954
+ messagesInflight.set(sid, p);
12955
+ return p;
12923
12956
  }
12924
12957
  function roleOf(msg) {
12925
12958
  if (!msg)
@@ -12971,6 +13004,43 @@ var AutoResumePlugin = async (ctx, options) => {
12971
13004
  return {};
12972
13005
  }
12973
13006
  }
13007
+ const todoCache = new Map;
13008
+ const todoInflight = new Map;
13009
+ const TODO_CACHE_TTL_MS = 2000;
13010
+ async function fetchSessionTodos(sid) {
13011
+ if (typeof sid !== "string" || !sid.startsWith("ses"))
13012
+ return [];
13013
+ const cached2 = todoCache.get(sid);
13014
+ if (cached2 && Date.now() - cached2.fetchedAt < TODO_CACHE_TTL_MS)
13015
+ return cached2.todos;
13016
+ const inflight = todoInflight.get(sid);
13017
+ if (inflight)
13018
+ return inflight;
13019
+ const p = (async () => {
13020
+ try {
13021
+ const todoFn = ctx.client.session.todo;
13022
+ if (typeof todoFn !== "function")
13023
+ return [];
13024
+ const response = await todoFn.call(ctx.client.session, { path: { id: sid } });
13025
+ const rawTodos = response.data ?? response;
13026
+ if (!Array.isArray(rawTodos))
13027
+ return [];
13028
+ const todos = rawTodos.map((t) => ({
13029
+ content: t?.content ?? "",
13030
+ status: t?.status ?? "pending",
13031
+ priority: t?.priority ?? "medium"
13032
+ }));
13033
+ todoCache.set(sid, { todos, fetchedAt: Date.now() });
13034
+ return todos;
13035
+ } catch {
13036
+ return [];
13037
+ } finally {
13038
+ todoInflight.delete(sid);
13039
+ }
13040
+ })();
13041
+ todoInflight.set(sid, p);
13042
+ return p;
13043
+ }
12974
13044
  const SUBAGENT_STUCK_MS = 60000;
12975
13045
  const SUBAGENT_RECOVERY_PROMPT = "It looks like you may have stalled or timed out. Please retry the last operation or continue with the task.";
12976
13046
  async function recoverSubagent(subagentSid) {
@@ -13096,6 +13166,34 @@ var AutoResumePlugin = async (ctx, options) => {
13096
13166
  w.recoveryAttempts = 0;
13097
13167
  w.watchdogRetryGuard = false;
13098
13168
  }
13169
+ function resetBusyFlags(w) {
13170
+ w.resumeAttempts = 0;
13171
+ w.lastRetryAt = 0;
13172
+ w.pendingTools = 0;
13173
+ w.pendingCommands = 0;
13174
+ w.gaveUp = false;
13175
+ w.orphanWatchStartAt = null;
13176
+ w.aborting = false;
13177
+ w.toolTextRecovered = false;
13178
+ w.toolTextAttempts = 0;
13179
+ w.todoCheckAttempts = 0;
13180
+ w.checkingToolText = false;
13181
+ w.interruptedContinueCount = 0;
13182
+ w.recentToolCalls = [];
13183
+ w.toolLoopAttempts = 0;
13184
+ w.pendingRecovery = false;
13185
+ w.pendingRecoveryReason = null;
13186
+ w.pendingRecoveryAt = 0;
13187
+ w.recoveryAttempts = 0;
13188
+ w.watchdogRetryGuard = false;
13189
+ if (w.toolTextTimer) {
13190
+ clearTimeout(w.toolTextTimer);
13191
+ w.toolTextTimer = null;
13192
+ }
13193
+ w.todoNudgeAttempts = 0;
13194
+ w.doneClaimNoTodosAttempts = 0;
13195
+ w.continueTimestamps = [];
13196
+ }
13099
13197
  function resetIdleFlags(w) {
13100
13198
  w.aborting = false;
13101
13199
  w.orphanWatchStartAt = null;
@@ -13331,14 +13429,19 @@ var AutoResumePlugin = async (ctx, options) => {
13331
13429
  const trimmedText = allAssistantText.trim();
13332
13430
  const normalized = trimmedText.replace(/[.!?]+$/, "");
13333
13431
  if (normalized.endsWith("\uD83C\uDF89") && (!bestCandidate || bestCandidate.priority > 0)) {
13334
- await log("info", `${short(sid)} - \uD83C\uDF89 completion detected, skipping continue`);
13335
- w.toolTextRecovered = true;
13336
- w.completionSignaled = true;
13337
- if (w.toolTextTimer) {
13338
- clearTimeout(w.toolTextTimer);
13339
- w.toolTextTimer = null;
13432
+ const openCount = getOpenTodos(w.todos || []).length;
13433
+ if (openCount > 0) {
13434
+ await log("info", `${short(sid)} - \uD83C\uDF89 detected but ${openCount} open todos remain, NOT latching completion`);
13435
+ } else {
13436
+ await log("info", `${short(sid)} - \uD83C\uDF89 completion detected, skipping continue`);
13437
+ w.toolTextRecovered = true;
13438
+ w.completionSignaled = true;
13439
+ if (w.toolTextTimer) {
13440
+ clearTimeout(w.toolTextTimer);
13441
+ w.toolTextTimer = null;
13442
+ }
13443
+ return;
13340
13444
  }
13341
- return;
13342
13445
  }
13343
13446
  if (!bestCandidate) {
13344
13447
  const todos = w.todos || [];
@@ -13412,9 +13515,10 @@ var AutoResumePlugin = async (ctx, options) => {
13412
13515
  await log("warn", `Invalid sid for abort: ${sid} (must start with "ses_")`);
13413
13516
  return false;
13414
13517
  }
13518
+ if (w.userCancelled || w.completionSignaled)
13519
+ return false;
13415
13520
  if (w.aborting)
13416
13521
  return false;
13417
- w.aborting = true;
13418
13522
  const idleSec = Math.round((Date.now() - (w.orphanWatchStartAt ?? w.lastActivityAt)) / 1000);
13419
13523
  await log("info", `Abort+Resume on ${short(sid)} (${idleSec}s idle). Aborting...`);
13420
13524
  try {
@@ -13500,7 +13604,12 @@ var AutoResumePlugin = async (ctx, options) => {
13500
13604
  w.idleSince = Date.now();
13501
13605
  }
13502
13606
  if (isNew) {
13503
- log("debug", `Discovered session ${short(sid)} via list()`);
13607
+ log("debug", `Discovered session ${short(sid)} via list() \u2014 fetching todos`);
13608
+ const fetched = await fetchSessionTodos(sid);
13609
+ if (fetched.length > 0) {
13610
+ const w = sessions.get(sid);
13611
+ w.todos = fetched;
13612
+ }
13504
13613
  }
13505
13614
  }
13506
13615
  }
@@ -13526,7 +13635,7 @@ var AutoResumePlugin = async (ctx, options) => {
13526
13635
  }
13527
13636
  if (w.status !== "busy")
13528
13637
  continue;
13529
- if (w.userCancelled)
13638
+ if (w.userCancelled || w.completionSignaled)
13530
13639
  continue;
13531
13640
  if (w.aborting)
13532
13641
  continue;
@@ -13666,6 +13775,12 @@ var AutoResumePlugin = async (ctx, options) => {
13666
13775
  continue;
13667
13776
  if (busyCount() !== 0)
13668
13777
  continue;
13778
+ if ((w.todos || []).length === 0) {
13779
+ const fetched = await fetchSessionTodos(sid);
13780
+ if (fetched.length > 0) {
13781
+ w.todos = fetched;
13782
+ }
13783
+ }
13669
13784
  const open = getOpenTodos(w.todos || []);
13670
13785
  if (open.length === 0)
13671
13786
  continue;
@@ -13677,8 +13792,13 @@ var AutoResumePlugin = async (ctx, options) => {
13677
13792
  continue;
13678
13793
  const isCelebration = await lastAssistantEndsWithCelebration(sid);
13679
13794
  if (isCelebration) {
13680
- w.toolTextRecovered = true;
13681
- w.completionSignaled = true;
13795
+ const openCount = getOpenTodos(w.todos || []).length;
13796
+ if (openCount > 0) {
13797
+ await log("info", `${short(sid)} - \uD83C\uDF89 detected in periodic recheck but ${openCount} open todos remain, NOT latching completion`);
13798
+ } else {
13799
+ w.toolTextRecovered = true;
13800
+ w.completionSignaled = true;
13801
+ }
13682
13802
  continue;
13683
13803
  }
13684
13804
  const reminder = buildOpenTodosReminder(w.todos || []);
@@ -13719,7 +13839,7 @@ var AutoResumePlugin = async (ctx, options) => {
13719
13839
  if (w.pendingRecovery) {
13720
13840
  dbg(`Pending recovery cleared on ${short(sid)}: reason=session-busy`);
13721
13841
  }
13722
- resetSessionFlags(w);
13842
+ resetBusyFlags(w);
13723
13843
  prevBusyCount = busyCount();
13724
13844
  log("debug", `${short(sid)} -> busy (${prevBusyCount})`);
13725
13845
  } else if (statusType === "interrupted") {
@@ -13763,23 +13883,27 @@ var AutoResumePlugin = async (ctx, options) => {
13763
13883
  dbg(`session.idle sid=${short(sid)}: streaming-failure check error: ${errMsg}`);
13764
13884
  }
13765
13885
  }
13766
- const todos = w.todos || [];
13886
+ let todos = w.todos || [];
13887
+ if (todos.length === 0) {
13888
+ const fetched = await fetchSessionTodos(sid);
13889
+ if (fetched.length > 0) {
13890
+ w.todos = fetched;
13891
+ todos = fetched;
13892
+ }
13893
+ }
13767
13894
  const open = getOpenTodos(todos);
13768
13895
  if (open.length > 0 && currentBusy === 0 && !w.completionSignaled && !w.userCancelled && w.todoNudgeAttempts < maxRetries) {
13769
13896
  const isCelebration = await lastAssistantEndsWithCelebration(sid);
13897
+ await log("info", `${short(sid)} - open todos=${open.length}, isCelebration=${isCelebration}, currentBusy=${currentBusy}`);
13770
13898
  if (isCelebration) {
13771
- await log("info", `${short(sid)} - \uD83C\uDF89 detected in idle handler, skipping continue`);
13772
- w.toolTextRecovered = true;
13773
- w.completionSignaled = true;
13774
- if (w.toolTextTimer) {
13775
- clearTimeout(w.toolTextTimer);
13776
- w.toolTextTimer = null;
13777
- }
13778
- } else {
13899
+ await log("info", `${short(sid)} - \uD83C\uDF89 detected but ${open.length} open todos remain, sending nudge`);
13900
+ const reminder = buildOpenTodosReminder(todos);
13901
+ await tryResume(sid, w, "Idle with open todos (celebration false positive)", reminder);
13779
13902
  w.todoNudgeAttempts++;
13903
+ } else {
13780
13904
  const reminder = buildOpenTodosReminder(todos);
13781
- await log("info", `${short(sid)} - idle with ${open.length} open todos. Sending reminder (nudge ${w.todoNudgeAttempts}/${maxRetries})...`);
13782
13905
  await tryResume(sid, w, "Idle with open todos", reminder);
13906
+ w.todoNudgeAttempts++;
13783
13907
  }
13784
13908
  }
13785
13909
  }
@@ -13793,8 +13917,7 @@ var AutoResumePlugin = async (ctx, options) => {
13793
13917
  dbg(`session.idle sid=${short(idleSid)}: skipping action intent, session is warming up (${Date.now() - idleW.createdAt}ms < ${warmupMs}ms)`);
13794
13918
  return;
13795
13919
  }
13796
- const response = await ctx.client.session.messages({ path: { id: idleSid } });
13797
- const msgs = extractMessages(response);
13920
+ const msgs = await getSessionMessages(idleSid);
13798
13921
  const lastAssistantMsg = msgs.slice().reverse().find((m) => (m.role ?? m.info?.role) === "assistant");
13799
13922
  if (lastAssistantMsg) {
13800
13923
  let lastText = "";
@@ -13804,7 +13927,7 @@ var AutoResumePlugin = async (ctx, options) => {
13804
13927
  }
13805
13928
  if (containsActionIntent(lastText)) {
13806
13929
  const w2 = sessions.get(idleSid);
13807
- if (!w2 || w2.toolTextRecovered || w2.completionSignaled || w2.status !== "idle")
13930
+ if (!w2 || w2.toolTextRecovered || w2.completionSignaled || w2.userCancelled || w2.status !== "idle")
13808
13931
  return;
13809
13932
  w2.toolTextRecovered = true;
13810
13933
  w2.toolTextAttempts++;
@@ -13858,8 +13981,7 @@ var AutoResumePlugin = async (ctx, options) => {
13858
13981
  dbg(`session.idle sid=${short(sid)}: skipping action intent, session is warming up (${Date.now() - w.createdAt}ms < ${warmupMs}ms)`);
13859
13982
  return;
13860
13983
  }
13861
- const response = await ctx.client.session.messages({ path: { id: sid } });
13862
- const msgs = extractMessages(response);
13984
+ const msgs = await getSessionMessages(sid);
13863
13985
  const lastAssistantMsg = msgs.slice().reverse().find((m) => (m.role ?? m.info?.role) === "assistant");
13864
13986
  if (lastAssistantMsg) {
13865
13987
  let lastText = "";
@@ -13869,7 +13991,7 @@ var AutoResumePlugin = async (ctx, options) => {
13869
13991
  }
13870
13992
  if (containsActionIntent(lastText)) {
13871
13993
  const w2 = sessions.get(sid);
13872
- if (!w2 || w2.toolTextRecovered || w2.completionSignaled || w2.status !== "idle")
13994
+ if (!w2 || w2.toolTextRecovered || w2.completionSignaled || w2.userCancelled || w2.status !== "idle")
13873
13995
  return;
13874
13996
  w2.toolTextRecovered = true;
13875
13997
  w2.toolTextAttempts++;
@@ -13968,16 +14090,14 @@ var AutoResumePlugin = async (ctx, options) => {
13968
14090
  break;
13969
14091
  }
13970
14092
  case "command.executed": {
13971
- for (const [sid2, w2] of sessions) {
13972
- if (w2.pendingRecovery) {
13973
- dbg(`Pending recovery cleared on ${short(sid2)}: reason=user-command`);
13974
- }
13975
- resetSessionFlags(w2);
13976
- }
13977
14093
  if (!sid)
13978
14094
  break;
13979
14095
  const w = sessions.get(sid);
13980
14096
  if (w) {
14097
+ if (w.pendingRecovery) {
14098
+ dbg(`Pending recovery cleared on ${short(sid)}: reason=user-command`);
14099
+ }
14100
+ resetBusyFlags(w);
13981
14101
  w.pendingCommands = Math.max(0, w.pendingCommands - 1);
13982
14102
  w.lastActivityAt = Date.now();
13983
14103
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-auto-resume",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "description": "OpenCode plugin that automatically resumes stalled LLM sessions when thinking/streaming freezes mid-generation.",
5
5
  "keywords": [
6
6
  "opencode",