opencode-auto-resume 1.0.24 → 1.1.2

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 +87 -27
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12346,6 +12346,7 @@ var ABORT_CONTINUE_DELAY_MS = 2000;
12346
12346
  var DEFAULT_LOOP_MAX_CONTINUES = 3;
12347
12347
  var DEFAULT_LOOP_WINDOW_MS = 10 * 60000;
12348
12348
  var TOOL_TEXT_CHECK_DELAY_MS = 3000;
12349
+ var MIN_ACTIVITY_GAP_MS = 1000;
12349
12350
  var MAX_IDLE_SESSIONS = 50;
12350
12351
  var IDLE_CLEANUP_MS = 10 * 60000;
12351
12352
  var SESSION_DISCOVERY_INTERVAL_MS = 60000;
@@ -12428,6 +12429,18 @@ function containsDoneClaimPattern(text) {
12428
12429
  `);
12429
12430
  return DONE_CLAIM_PATTERNS.some((pat) => pat.test(lastLines));
12430
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
+ }
12431
12444
  var AutoResumePlugin = async (ctx, options) => {
12432
12445
  const chunkTimeoutMs = options?.chunkTimeoutMs ?? DEFAULT_CHUNK_TIMEOUT_MS;
12433
12446
  const checkIntervalMs = options?.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS;
@@ -12490,7 +12503,9 @@ var AutoResumePlugin = async (ctx, options) => {
12490
12503
  interruptedContinueCount: 0,
12491
12504
  recentToolCalls: [],
12492
12505
  toolLoopAttempts: 0,
12493
- isSubagent: false
12506
+ isSubagent: false,
12507
+ completionSignaled: false,
12508
+ todoNudgeAttempts: 0
12494
12509
  };
12495
12510
  sessions.set(sid, w);
12496
12511
  }
@@ -12679,6 +12694,28 @@ var AutoResumePlugin = async (ctx, options) => {
12679
12694
  return;
12680
12695
  return msg.role ?? msg.info?.role;
12681
12696
  }
12697
+ async function lastAssistantEndsWithCelebration(sid) {
12698
+ try {
12699
+ const msgs = await getSessionMessages(sid);
12700
+ for (let i = msgs.length - 1;i >= 0; i--) {
12701
+ const msg = msgs[i];
12702
+ if (roleOf(msg) !== "assistant")
12703
+ continue;
12704
+ const parts = msg.parts;
12705
+ if (!parts)
12706
+ continue;
12707
+ let text = "";
12708
+ for (const part of parts) {
12709
+ if (part.type === "text") {
12710
+ text += part.text ?? "";
12711
+ }
12712
+ }
12713
+ const normalized = text.trim().replace(/[.!?]+$/, "");
12714
+ return normalized.endsWith("\uD83C\uDF89");
12715
+ }
12716
+ } catch {}
12717
+ return false;
12718
+ }
12682
12719
  async function getSessionStatusMap() {
12683
12720
  try {
12684
12721
  const response = await ctx.client.session.status();
@@ -12804,10 +12841,10 @@ var AutoResumePlugin = async (ctx, options) => {
12804
12841
  w.gaveUp = false;
12805
12842
  w.orphanWatchStartAt = null;
12806
12843
  w.aborting = false;
12807
- if (w.isSubagent) {
12808
- w.toolTextRecovered = false;
12809
- }
12844
+ w.toolTextRecovered = false;
12810
12845
  w.toolTextAttempts = 0;
12846
+ w.completionSignaled = false;
12847
+ w.todoNudgeAttempts = 0;
12811
12848
  w.continueTimestamps = [];
12812
12849
  w.idleSince = null;
12813
12850
  w.continuing = false;
@@ -13028,6 +13065,7 @@ var AutoResumePlugin = async (ctx, options) => {
13028
13065
  if (normalized.endsWith("\uD83C\uDF89") && (!bestCandidate || bestCandidate.priority > 0)) {
13029
13066
  await log("info", `${short(sid)} - \uD83C\uDF89 completion detected, skipping continue`);
13030
13067
  w.toolTextRecovered = true;
13068
+ w.completionSignaled = true;
13031
13069
  if (w.toolTextTimer) {
13032
13070
  clearTimeout(w.toolTextTimer);
13033
13071
  w.toolTextTimer = null;
@@ -13037,24 +13075,34 @@ var AutoResumePlugin = async (ctx, options) => {
13037
13075
  if (!bestCandidate) {
13038
13076
  const todos = w.todos || [];
13039
13077
  const hasOpenTodos = todos.some((t) => t.status === "pending" || t.status === "in_progress");
13040
- if (hasOpenTodos && busyCount() === 1) {
13041
- 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...`);
13078
+ if (hasOpenTodos && busyCount() === 0) {
13079
+ const reminder = buildOpenTodosReminder(todos);
13080
+ 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...`);
13042
13081
  bestCandidate = {
13043
- prompt: "continue",
13044
- source: "idle-with-open-todos",
13082
+ prompt: reminder,
13083
+ source: "idle-with-open-todos-reminder",
13045
13084
  priority: 2
13046
13085
  };
13047
- } else if (hasOpenTodos && busyCount() > 1) {
13086
+ } else if (hasOpenTodos && busyCount() > 0) {
13048
13087
  await log("debug", `${short(sid)} - todos remain open but ${busyCount()} sessions busy (subagents running), skipping continue`);
13049
13088
  }
13050
13089
  }
13051
13090
  if (!bestCandidate)
13052
13091
  return;
13053
- w.toolTextRecovered = true;
13054
- w.toolTextAttempts++;
13055
- await log("info", `${bestCandidate.source} detected on ${short(sid)}! ` + `Attempt ${w.toolTextAttempts}/${maxRetries}. Sending recovery prompt...`);
13092
+ const isOpenTodosReminder = bestCandidate.source === "idle-with-open-todos-reminder";
13093
+ if (isOpenTodosReminder) {
13094
+ if (w.todoNudgeAttempts >= maxRetries) {
13095
+ await log("info", `${short(sid)} - max open-todos nudges (${maxRetries}) reached for this idle cycle, waiting for activity`);
13096
+ return;
13097
+ }
13098
+ w.todoNudgeAttempts++;
13099
+ } else {
13100
+ w.toolTextRecovered = true;
13101
+ w.toolTextAttempts++;
13102
+ }
13103
+ await log("info", `${bestCandidate.source} detected on ${short(sid)}! ` + `Attempt ${isOpenTodosReminder ? w.todoNudgeAttempts : w.toolTextAttempts}/${maxRetries}. Sending recovery prompt...`);
13056
13104
  const timeSinceActivity = Date.now() - w.lastActivityAt;
13057
- if (timeSinceActivity < TOOL_TEXT_CHECK_DELAY_MS) {
13105
+ if (timeSinceActivity < MIN_ACTIVITY_GAP_MS) {
13058
13106
  await log("info", `${short(sid)} - skipping ${bestCandidate.source}, session was active ${Math.round(timeSinceActivity / 1000)}s ago`);
13059
13107
  return;
13060
13108
  }
@@ -13118,7 +13166,7 @@ var AutoResumePlugin = async (ctx, options) => {
13118
13166
  return false;
13119
13167
  }
13120
13168
  }
13121
- async function tryResume(sid, w, reason) {
13169
+ async function tryResume(sid, w, reason, prompt) {
13122
13170
  if (typeof sid !== "string" || !sid) {
13123
13171
  await log("warn", `tryResume called with invalid sid: ${sid}`);
13124
13172
  return false;
@@ -13142,7 +13190,7 @@ var AutoResumePlugin = async (ctx, options) => {
13142
13190
  const idleSec = Math.round((now - w.lastActivityAt) / 1000);
13143
13191
  await log("info", `${reason} on ${short(sid)} (${idleSec}s, retry ${w.resumeAttempts}/${maxRetries})`);
13144
13192
  try {
13145
- await sendContinuePrompt(sid, "continue", w);
13193
+ await sendContinuePrompt(sid, prompt ?? "continue", w);
13146
13194
  await log("info", `${short(sid)} - retry sent`);
13147
13195
  return true;
13148
13196
  } catch (err) {
@@ -13331,12 +13379,24 @@ var AutoResumePlugin = async (ctx, options) => {
13331
13379
  if (!w.isSubagent) {
13332
13380
  const todos = w.todos || [];
13333
13381
  const hasOpenTodos = todos.some((t) => t.status === "pending" || t.status === "in_progress");
13334
- if (hasOpenTodos && currentBusy === 1 && !w.toolTextRecovered) {
13335
- await log("info", `${short(sid)} - idle with ${todos.filter((t) => t.status === "pending" || t.status === "in_progress").length} open todos. Sending continue...`);
13336
- tryResume(sid, w, "Idle with open todos");
13382
+ if (hasOpenTodos && currentBusy === 0 && !w.completionSignaled) {
13383
+ const isCelebration = await lastAssistantEndsWithCelebration(sid);
13384
+ if (isCelebration) {
13385
+ await log("info", `${short(sid)} - \uD83C\uDF89 detected in idle handler, skipping continue`);
13386
+ w.toolTextRecovered = true;
13387
+ w.completionSignaled = true;
13388
+ if (w.toolTextTimer) {
13389
+ clearTimeout(w.toolTextTimer);
13390
+ w.toolTextTimer = null;
13391
+ }
13392
+ } else {
13393
+ const reminder = buildOpenTodosReminder(todos);
13394
+ await log("info", `${short(sid)} - idle with ${todos.filter((t) => t.status === "pending" || t.status === "in_progress").length} open todos. Sending reminder...`);
13395
+ tryResume(sid, w, "Idle with open todos", reminder);
13396
+ }
13337
13397
  }
13338
13398
  }
13339
- if (!w.toolTextRecovered && w.toolTextAttempts < maxRetries) {
13399
+ if (!w.completionSignaled && w.toolTextAttempts < maxRetries) {
13340
13400
  if (w.toolTextTimer)
13341
13401
  clearTimeout(w.toolTextTimer);
13342
13402
  const checkDelay = statusType === "interrupted" ? 500 : TOOL_TEXT_CHECK_DELAY_MS;
@@ -13419,14 +13479,12 @@ var AutoResumePlugin = async (ctx, options) => {
13419
13479
  break;
13420
13480
  const props = ev.properties;
13421
13481
  const todos = props?.todos ?? [];
13422
- const w = sessions.get(sid);
13423
- if (w) {
13424
- w.todos = todos.map((t) => ({
13425
- content: t.content ?? "",
13426
- status: t.status ?? "pending",
13427
- priority: t.priority ?? "medium"
13428
- }));
13429
- }
13482
+ const w = ensureWatch(sid);
13483
+ w.todos = todos.map((t) => ({
13484
+ content: t.content ?? "",
13485
+ status: t.status ?? "pending",
13486
+ priority: t.priority ?? "medium"
13487
+ }));
13430
13488
  break;
13431
13489
  }
13432
13490
  case "session.error": {
@@ -13466,6 +13524,7 @@ var AutoResumePlugin = async (ctx, options) => {
13466
13524
  if (w) {
13467
13525
  if (!w.isSubagent) {
13468
13526
  w.toolTextRecovered = true;
13527
+ w.completionSignaled = true;
13469
13528
  if (w.toolTextTimer) {
13470
13529
  clearTimeout(w.toolTextTimer);
13471
13530
  w.toolTextTimer = null;
@@ -13495,5 +13554,6 @@ var AutoResumePlugin = async (ctx, options) => {
13495
13554
  var src_default = AutoResumePlugin;
13496
13555
  export {
13497
13556
  src_default as default,
13557
+ buildOpenTodosReminder,
13498
13558
  AutoResumePlugin
13499
13559
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-auto-resume",
3
- "version": "1.0.24",
3
+ "version": "1.1.2",
4
4
  "description": "OpenCode plugin that automatically resumes stalled LLM sessions when thinking/streaming freezes mid-generation.",
5
5
  "keywords": [
6
6
  "opencode",