opencode-auto-resume 1.1.1 → 1.1.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.
Files changed (2) hide show
  1. package/dist/index.js +144 -56
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12429,6 +12429,18 @@ function containsDoneClaimPattern(text) {
12429
12429
  `);
12430
12430
  return DONE_CLAIM_PATTERNS.some((pat) => pat.test(lastLines));
12431
12431
  }
12432
+ function buildOpenTodosReminder(todos) {
12433
+ const open = todos.filter((t) => t.status === "pending" || t.status === "in_progress");
12434
+ if (open.length === 0)
12435
+ return "continue";
12436
+ const list = open.map((t, i) => `${i + 1}. [${t.status}] ${t.content}`).join(`
12437
+ `);
12438
+ const plural = open.length > 1 ? "s" : "";
12439
+ return `You have ${open.length} unfinished task${plural}:
12440
+ ${list}
12441
+
12442
+ Please continue working on these task${plural}.`;
12443
+ }
12432
12444
  var AutoResumePlugin = async (ctx, options) => {
12433
12445
  const chunkTimeoutMs = options?.chunkTimeoutMs ?? DEFAULT_CHUNK_TIMEOUT_MS;
12434
12446
  const checkIntervalMs = options?.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS;
@@ -12491,7 +12503,11 @@ var AutoResumePlugin = async (ctx, options) => {
12491
12503
  interruptedContinueCount: 0,
12492
12504
  recentToolCalls: [],
12493
12505
  toolLoopAttempts: 0,
12494
- isSubagent: false
12506
+ isSubagent: false,
12507
+ completionSignaled: false,
12508
+ todoNudgeAttempts: 0,
12509
+ pendingTools: 0,
12510
+ pendingCommands: 0
12495
12511
  };
12496
12512
  sessions.set(sid, w);
12497
12513
  }
@@ -12503,6 +12519,9 @@ var AutoResumePlugin = async (ctx, options) => {
12503
12519
  w.lastActivityAt = Date.now();
12504
12520
  }
12505
12521
  }
12522
+ function hasInflightTools(w) {
12523
+ return w.pendingTools > 0 || w.pendingCommands > 0;
12524
+ }
12506
12525
  function busyCount() {
12507
12526
  let count = 0;
12508
12527
  for (const [, w] of sessions) {
@@ -12824,13 +12843,15 @@ var AutoResumePlugin = async (ctx, options) => {
12824
12843
  function resetSessionFlags(w) {
12825
12844
  w.userCancelled = false;
12826
12845
  w.resumeAttempts = 0;
12846
+ w.pendingTools = 0;
12847
+ w.pendingCommands = 0;
12827
12848
  w.gaveUp = false;
12828
12849
  w.orphanWatchStartAt = null;
12829
12850
  w.aborting = false;
12830
- if (w.isSubagent) {
12831
- w.toolTextRecovered = false;
12832
- }
12851
+ w.toolTextRecovered = false;
12833
12852
  w.toolTextAttempts = 0;
12853
+ w.completionSignaled = false;
12854
+ w.todoNudgeAttempts = 0;
12834
12855
  w.continueTimestamps = [];
12835
12856
  w.idleSince = null;
12836
12857
  w.continuing = false;
@@ -12845,10 +12866,11 @@ var AutoResumePlugin = async (ctx, options) => {
12845
12866
  }
12846
12867
  }
12847
12868
  function resetIdleFlags(w) {
12848
- w.userCancelled = false;
12849
12869
  w.aborting = false;
12850
12870
  w.orphanWatchStartAt = null;
12851
12871
  w.idleSince = Date.now();
12872
+ w.pendingTools = 0;
12873
+ w.pendingCommands = 0;
12852
12874
  }
12853
12875
  function detectPatternLoop(recentTools) {
12854
12876
  if (recentTools.length < 6)
@@ -13051,6 +13073,7 @@ var AutoResumePlugin = async (ctx, options) => {
13051
13073
  if (normalized.endsWith("\uD83C\uDF89") && (!bestCandidate || bestCandidate.priority > 0)) {
13052
13074
  await log("info", `${short(sid)} - \uD83C\uDF89 completion detected, skipping continue`);
13053
13075
  w.toolTextRecovered = true;
13076
+ w.completionSignaled = true;
13054
13077
  if (w.toolTextTimer) {
13055
13078
  clearTimeout(w.toolTextTimer);
13056
13079
  w.toolTextTimer = null;
@@ -13061,10 +13084,11 @@ var AutoResumePlugin = async (ctx, options) => {
13061
13084
  const todos = w.todos || [];
13062
13085
  const hasOpenTodos = todos.some((t) => t.status === "pending" || t.status === "in_progress");
13063
13086
  if (hasOpenTodos && busyCount() === 0) {
13064
- 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...`);
13087
+ const reminder = buildOpenTodosReminder(todos);
13088
+ 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 reminder...`);
13065
13089
  bestCandidate = {
13066
- prompt: "continue",
13067
- source: "idle-with-open-todos",
13090
+ prompt: reminder,
13091
+ source: "idle-with-open-todos-reminder",
13068
13092
  priority: 2
13069
13093
  };
13070
13094
  } else if (hasOpenTodos && busyCount() > 0) {
@@ -13073,15 +13097,28 @@ var AutoResumePlugin = async (ctx, options) => {
13073
13097
  }
13074
13098
  if (!bestCandidate)
13075
13099
  return;
13076
- w.toolTextRecovered = true;
13077
- w.toolTextAttempts++;
13078
- await log("info", `${bestCandidate.source} detected on ${short(sid)}! ` + `Attempt ${w.toolTextAttempts}/${maxRetries}. Sending recovery prompt...`);
13100
+ const isOpenTodosReminder = bestCandidate.source === "idle-with-open-todos-reminder";
13101
+ if (isOpenTodosReminder) {
13102
+ if (w.todoNudgeAttempts >= maxRetries) {
13103
+ await log("info", `${short(sid)} - max open-todos nudges (${maxRetries}) reached for this idle cycle, waiting for activity`);
13104
+ return;
13105
+ }
13106
+ w.todoNudgeAttempts++;
13107
+ } else {
13108
+ w.toolTextRecovered = true;
13109
+ w.toolTextAttempts++;
13110
+ }
13111
+ await log("info", `${bestCandidate.source} detected on ${short(sid)}! ` + `Attempt ${isOpenTodosReminder ? w.todoNudgeAttempts : w.toolTextAttempts}/${maxRetries}. Sending recovery prompt...`);
13079
13112
  const timeSinceActivity = Date.now() - w.lastActivityAt;
13080
13113
  if (timeSinceActivity < MIN_ACTIVITY_GAP_MS) {
13081
13114
  await log("info", `${short(sid)} - skipping ${bestCandidate.source}, session was active ${Math.round(timeSinceActivity / 1000)}s ago`);
13082
13115
  return;
13083
13116
  }
13084
13117
  if (isHallucinationLoop(sid)) {
13118
+ if (hasInflightTools(w)) {
13119
+ await log("debug", `Session ${short(sid)} has ${w.pendingTools} tool(s) in-flight, skipping hallucination abort`);
13120
+ return;
13121
+ }
13085
13122
  const hasActiveTool = await checkSessionHasActiveTool(sid);
13086
13123
  if (hasActiveTool) {
13087
13124
  await log("debug", `Session ${short(sid)} has active tool, skipping hallucination abort`);
@@ -13141,7 +13178,7 @@ var AutoResumePlugin = async (ctx, options) => {
13141
13178
  return false;
13142
13179
  }
13143
13180
  }
13144
- async function tryResume(sid, w, reason) {
13181
+ async function tryResume(sid, w, reason, prompt) {
13145
13182
  if (typeof sid !== "string" || !sid) {
13146
13183
  await log("warn", `tryResume called with invalid sid: ${sid}`);
13147
13184
  return false;
@@ -13152,6 +13189,11 @@ var AutoResumePlugin = async (ctx, options) => {
13152
13189
  if (w.lastRetryAt > 0 && elapsedSinceRetry < requiredBackoff)
13153
13190
  return false;
13154
13191
  if (isHallucinationLoop(sid)) {
13192
+ if (hasInflightTools(w)) {
13193
+ await log("debug", `Session ${short(sid)} has ${w.pendingTools} tool(s) in-flight, skipping hallucination abort`);
13194
+ w.lastRetryAt = now;
13195
+ return false;
13196
+ }
13155
13197
  const hasActiveTool = await checkSessionHasActiveTool(sid);
13156
13198
  if (hasActiveTool) {
13157
13199
  await log("debug", `Session ${short(sid)} has active tool, skipping hallucination abort`);
@@ -13165,7 +13207,7 @@ var AutoResumePlugin = async (ctx, options) => {
13165
13207
  const idleSec = Math.round((now - w.lastActivityAt) / 1000);
13166
13208
  await log("info", `${reason} on ${short(sid)} (${idleSec}s, retry ${w.resumeAttempts}/${maxRetries})`);
13167
13209
  try {
13168
- await sendContinuePrompt(sid, "continue", w);
13210
+ await sendContinuePrompt(sid, prompt ?? "continue", w);
13169
13211
  await log("info", `${short(sid)} - retry sent`);
13170
13212
  return true;
13171
13213
  } catch (err) {
@@ -13225,6 +13267,11 @@ var AutoResumePlugin = async (ctx, options) => {
13225
13267
  const orphanIdle = now - w.orphanWatchStartAt;
13226
13268
  if (orphanIdle >= subagentWaitMs + gracePeriodMs) {
13227
13269
  if (w.resumeAttempts < maxRetries) {
13270
+ if (hasInflightTools(w)) {
13271
+ await log("debug", `Parent ${short(sid)} has ${w.pendingTools} tool(s) in-flight, skipping orphan-watch abort`);
13272
+ w.orphanWatchStartAt = now;
13273
+ continue;
13274
+ }
13228
13275
  const hasActiveTool = await checkSessionHasActiveTool(sid);
13229
13276
  if (hasActiveTool) {
13230
13277
  await log("debug", `Parent ${short(sid)} has active tool call, skipping orphan-watch abort`);
@@ -13271,6 +13318,11 @@ var AutoResumePlugin = async (ctx, options) => {
13271
13318
  w.lastSubagentCheckAt = now;
13272
13319
  continue;
13273
13320
  }
13321
+ if (hasInflightTools(w)) {
13322
+ await log("debug", `Session ${short(sid)} has ${w.pendingTools} tool(s) in-flight, skipping abort check`);
13323
+ w.lastSubagentCheckAt = now;
13324
+ continue;
13325
+ }
13274
13326
  const hasActiveTool = await checkSessionHasActiveTool(sid);
13275
13327
  if (hasActiveTool) {
13276
13328
  await log("debug", `Session ${short(sid)} has active tool call, skipping abort check`);
@@ -13295,15 +13347,20 @@ var AutoResumePlugin = async (ctx, options) => {
13295
13347
  }
13296
13348
  const idle = now - w.lastActivityAt;
13297
13349
  if (idle >= chunkTimeoutMs + gracePeriodMs) {
13298
- const hasActiveTool = await checkSessionHasActiveTool(sid);
13299
- if (hasActiveTool) {
13300
- await log("debug", `Session ${short(sid)} has active tool call, skipping stall recovery`);
13350
+ if (hasInflightTools(w)) {
13351
+ await log("debug", `Session ${short(sid)} has ${w.pendingTools} tool(s) in-flight, skipping stall recovery`);
13301
13352
  w.lastSubagentCheckAt = now;
13302
- } else if (w.resumeAttempts < maxRetries) {
13303
- tryResume(sid, w, "Stream stall");
13304
- } else if (!w.gaveUp) {
13305
- w.gaveUp = true;
13306
- log("warn", `${short(sid)} - all ${maxRetries} retries exhausted.`);
13353
+ } else {
13354
+ const hasActiveTool = await checkSessionHasActiveTool(sid);
13355
+ if (hasActiveTool) {
13356
+ await log("debug", `Session ${short(sid)} has active tool call, skipping stall recovery`);
13357
+ w.lastSubagentCheckAt = now;
13358
+ } else if (w.resumeAttempts < maxRetries) {
13359
+ tryResume(sid, w, "Stream stall");
13360
+ } else if (!w.gaveUp) {
13361
+ w.gaveUp = true;
13362
+ log("warn", `${short(sid)} - all ${maxRetries} retries exhausted.`);
13363
+ }
13307
13364
  }
13308
13365
  }
13309
13366
  }
@@ -13337,7 +13394,17 @@ var AutoResumePlugin = async (ctx, options) => {
13337
13394
  resetSessionFlags(w);
13338
13395
  prevBusyCount = busyCount();
13339
13396
  log("debug", `${short(sid)} -> busy (${prevBusyCount})`);
13340
- } else if (statusType === "idle" || statusType === "interrupted") {
13397
+ } else if (statusType === "interrupted") {
13398
+ w.status = "idle";
13399
+ resetIdleFlags(w);
13400
+ w.userCancelled = true;
13401
+ if (w.toolTextTimer) {
13402
+ clearTimeout(w.toolTextTimer);
13403
+ w.toolTextTimer = null;
13404
+ }
13405
+ prevBusyCount = busyCount();
13406
+ log("info", `${short(sid)} -> interrupted by user, backing off`);
13407
+ } else if (statusType === "idle") {
13341
13408
  w.status = "idle";
13342
13409
  resetIdleFlags(w);
13343
13410
  const currentBusy = busyCount();
@@ -13350,32 +13417,33 @@ var AutoResumePlugin = async (ctx, options) => {
13350
13417
  }
13351
13418
  }
13352
13419
  prevBusyCount = currentBusy;
13353
- log("debug", `${short(sid)} -> idle (${currentBusy})${statusType === "interrupted" ? " (interrupted)" : ""}`);
13420
+ log("debug", `${short(sid)} -> idle (${currentBusy})`);
13354
13421
  if (!w.isSubagent) {
13355
13422
  const todos = w.todos || [];
13356
13423
  const hasOpenTodos = todos.some((t) => t.status === "pending" || t.status === "in_progress");
13357
- if (hasOpenTodos && currentBusy === 0 && !w.toolTextRecovered) {
13424
+ if (hasOpenTodos && currentBusy === 0 && !w.completionSignaled && !w.userCancelled) {
13358
13425
  const isCelebration = await lastAssistantEndsWithCelebration(sid);
13359
13426
  if (isCelebration) {
13360
13427
  await log("info", `${short(sid)} - \uD83C\uDF89 detected in idle handler, skipping continue`);
13361
13428
  w.toolTextRecovered = true;
13429
+ w.completionSignaled = true;
13362
13430
  if (w.toolTextTimer) {
13363
13431
  clearTimeout(w.toolTextTimer);
13364
13432
  w.toolTextTimer = null;
13365
13433
  }
13366
13434
  } else {
13367
- await log("info", `${short(sid)} - idle with ${todos.filter((t) => t.status === "pending" || t.status === "in_progress").length} open todos. Sending continue...`);
13368
- tryResume(sid, w, "Idle with open todos");
13435
+ const reminder = buildOpenTodosReminder(todos);
13436
+ await log("info", `${short(sid)} - idle with ${todos.filter((t) => t.status === "pending" || t.status === "in_progress").length} open todos. Sending reminder...`);
13437
+ tryResume(sid, w, "Idle with open todos", reminder);
13369
13438
  }
13370
13439
  }
13371
13440
  }
13372
- if (!w.toolTextRecovered && w.toolTextAttempts < maxRetries) {
13441
+ if (!w.completionSignaled && !w.userCancelled && w.toolTextAttempts < maxRetries) {
13373
13442
  if (w.toolTextTimer)
13374
13443
  clearTimeout(w.toolTextTimer);
13375
- const checkDelay = statusType === "interrupted" ? 500 : TOOL_TEXT_CHECK_DELAY_MS;
13376
13444
  w.toolTextTimer = setTimeout(() => {
13377
13445
  checkForToolCallAsText(sid, w);
13378
- }, checkDelay);
13446
+ }, TOOL_TEXT_CHECK_DELAY_MS);
13379
13447
  }
13380
13448
  } else if (statusType === "retry") {
13381
13449
  touchSession(sid);
@@ -13386,7 +13454,9 @@ var AutoResumePlugin = async (ctx, options) => {
13386
13454
  case "session.created": {
13387
13455
  if (!sid)
13388
13456
  break;
13389
- ensureWatch(sid);
13457
+ const w = ensureWatch(sid);
13458
+ w.pendingTools = 0;
13459
+ w.pendingCommands = 0;
13390
13460
  log("debug", `New session: ${short(sid)} (${sessions.size})`);
13391
13461
  break;
13392
13462
  }
@@ -13417,33 +13487,14 @@ var AutoResumePlugin = async (ctx, options) => {
13417
13487
  break;
13418
13488
  const w = sessions.get(sid);
13419
13489
  if (w) {
13420
- const wasJustContinued = w.continuing || Date.now() - w.lastRetryAt < 2000;
13421
13490
  w.status = "idle";
13422
13491
  resetIdleFlags(w);
13423
- log("debug", `${short(sid)} -> interrupted${wasJustContinued ? " (after continue)" : ""}`);
13424
- if (wasJustContinued && w.interruptedContinueCount < 3 && !w.toolTextRecovered && w.toolTextAttempts < maxRetries) {
13425
- w.interruptedContinueCount++;
13426
- await log("info", `${short(sid)} - continue was interrupted (${w.interruptedContinueCount}/3), retrying...`);
13427
- w.continuing = false;
13428
- try {
13429
- await sendContinuePrompt(sid, "continue", w);
13430
- await log("info", `${short(sid)} - interrupted continue retried`);
13431
- } catch (err) {
13432
- const errMsg = err instanceof Error ? err.message : String(err);
13433
- await log("warn", `${short(sid)} - interrupted continue retry failed: ${errMsg}`);
13434
- }
13435
- return;
13436
- } else if (wasJustContinued && w.interruptedContinueCount >= 3) {
13437
- await log("warn", `${short(sid)} - too many interrupted continues (${w.interruptedContinueCount}), stopping retries`);
13438
- w.interruptedContinueCount = 0;
13439
- }
13440
- if (!w.toolTextRecovered && w.toolTextAttempts < maxRetries) {
13441
- if (w.toolTextTimer)
13442
- clearTimeout(w.toolTextTimer);
13443
- w.toolTextTimer = setTimeout(() => {
13444
- checkForToolCallAsText(sid, w);
13445
- }, 500);
13492
+ w.userCancelled = true;
13493
+ if (w.toolTextTimer) {
13494
+ clearTimeout(w.toolTextTimer);
13495
+ w.toolTextTimer = null;
13446
13496
  }
13497
+ log("info", `${short(sid)} -> interrupted by user, backing off`);
13447
13498
  }
13448
13499
  break;
13449
13500
  }
@@ -13479,11 +13530,25 @@ var AutoResumePlugin = async (ctx, options) => {
13479
13530
  break;
13480
13531
  const errorMessage = errorObj?.data?.message ?? String(errorObj?.data ?? "");
13481
13532
  log("debug", `Session error: ${errorName} - ${errorMessage}`);
13533
+ if (sid) {
13534
+ const w = sessions.get(sid);
13535
+ if (w) {
13536
+ w.pendingTools = 0;
13537
+ w.pendingCommands = 0;
13538
+ }
13539
+ }
13482
13540
  break;
13483
13541
  }
13484
13542
  case "command.executed": {
13485
- for (const [, w] of sessions) {
13486
- resetSessionFlags(w);
13543
+ for (const [, w2] of sessions) {
13544
+ resetSessionFlags(w2);
13545
+ }
13546
+ if (!sid)
13547
+ break;
13548
+ const w = sessions.get(sid);
13549
+ if (w) {
13550
+ w.pendingCommands = Math.max(0, w.pendingCommands - 1);
13551
+ w.lastActivityAt = Date.now();
13487
13552
  }
13488
13553
  break;
13489
13554
  }
@@ -13497,6 +13562,7 @@ var AutoResumePlugin = async (ctx, options) => {
13497
13562
  if (w) {
13498
13563
  if (!w.isSubagent) {
13499
13564
  w.toolTextRecovered = true;
13565
+ w.completionSignaled = true;
13500
13566
  if (w.toolTextTimer) {
13501
13567
  clearTimeout(w.toolTextTimer);
13502
13568
  w.toolTextTimer = null;
@@ -13520,11 +13586,33 @@ var AutoResumePlugin = async (ctx, options) => {
13520
13586
  },
13521
13587
  tool: {
13522
13588
  task_complete: taskCompleteTool
13589
+ },
13590
+ "tool.execute.before": async (input) => {
13591
+ if (!input?.sessionID)
13592
+ return;
13593
+ const w = ensureWatch(input.sessionID);
13594
+ w.pendingTools++;
13595
+ w.lastActivityAt = Date.now();
13596
+ },
13597
+ "command.execute.before": async (input) => {
13598
+ if (!input?.sessionID)
13599
+ return;
13600
+ const w = ensureWatch(input.sessionID);
13601
+ w.pendingCommands++;
13602
+ w.lastActivityAt = Date.now();
13603
+ },
13604
+ "tool.execute.after": async (input) => {
13605
+ if (!input?.sessionID)
13606
+ return;
13607
+ const w = ensureWatch(input.sessionID);
13608
+ w.pendingTools = Math.max(0, w.pendingTools - 1);
13609
+ w.lastActivityAt = Date.now();
13523
13610
  }
13524
13611
  };
13525
13612
  };
13526
13613
  var src_default = AutoResumePlugin;
13527
13614
  export {
13528
13615
  src_default as default,
13616
+ buildOpenTodosReminder,
13529
13617
  AutoResumePlugin
13530
13618
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-auto-resume",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "OpenCode plugin that automatically resumes stalled LLM sessions when thinking/streaming freezes mid-generation.",
5
5
  "keywords": [
6
6
  "opencode",