opencode-auto-resume 1.1.5 → 1.1.8

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 (3) hide show
  1. package/README.md +1 -16
  2. package/dist/index.js +185 -162
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -265,11 +265,7 @@ Periodic: cleanup idle sessions older than 10min or >50 entries
265
265
 
266
266
  ## Installation
267
267
 
268
- ### Via npm (recommended)
269
-
270
- ```bash
271
- npm install opencode-auto-resume
272
- ```
268
+ ### Opencode
273
269
 
274
270
  Add to your `opencode.jsonc`:
275
271
 
@@ -294,17 +290,6 @@ With options:
294
290
  }
295
291
  ```
296
292
 
297
- ### Via GitHub (manual clone)
298
-
299
- OpenCode may clone the repository to `~/.config/opencode/plugins/opencode-auto-resume/` automatically.
300
-
301
- **To update** the plugin:
302
- ```bash
303
- cd ~/.config/opencode/plugins/opencode-auto-resume
304
- git pull
305
- bun run build
306
- ```
307
-
308
293
  ## Configuration
309
294
 
310
295
  ```json
package/dist/index.js CHANGED
@@ -12523,6 +12523,8 @@ function isOpenTodo(t) {
12523
12523
  return t.status === "pending" || t.status === "in_progress";
12524
12524
  }
12525
12525
  function getOpenTodos(todos) {
12526
+ if (!Array.isArray(todos))
12527
+ return [];
12526
12528
  return todos.filter(isOpenTodo);
12527
12529
  }
12528
12530
  function buildOpenTodosReminder(todos) {
@@ -12593,7 +12595,21 @@ var AutoResumePlugin = async (ctx, options) => {
12593
12595
  async function log(level, msg) {
12594
12596
  try {
12595
12597
  await ctx.client.app.log({ body: { service: "auto-resume", level, message: msg } });
12596
- } catch {}
12598
+ } catch (e) {
12599
+ console.error("[auto-resume] log() failed:", e instanceof Error ? e.message : String(e));
12600
+ }
12601
+ }
12602
+ async function safe(fn, ctxLabel) {
12603
+ try {
12604
+ return await fn();
12605
+ } catch (e) {
12606
+ const msg = e instanceof Error ? e.message : String(e);
12607
+ console.error(`[auto-resume] ${ctxLabel}: ${msg}`);
12608
+ try {
12609
+ await log("error", `${ctxLabel}: ${msg}`);
12610
+ } catch {}
12611
+ return;
12612
+ }
12597
12613
  }
12598
12614
  function ensureWatch(sid) {
12599
12615
  let w = sessions.get(sid);
@@ -13497,186 +13513,188 @@ var AutoResumePlugin = async (ctx, options) => {
13497
13513
  if (timer)
13498
13514
  return;
13499
13515
  timer = setInterval(async () => {
13500
- const now = Date.now();
13501
- const numBusy = busyCount();
13502
- const statusMap = await getSessionStatusMap();
13503
- for (const [sid, w] of sessions) {
13504
- const realStatus = statusMap[sid];
13505
- if (realStatus && realStatus !== w.status) {
13506
- w.status = realStatus;
13507
- if (realStatus === "busy")
13508
- w.idleSince = null;
13509
- }
13510
- if (w.status !== "busy")
13511
- continue;
13512
- if (w.userCancelled)
13513
- continue;
13514
- if (w.aborting)
13515
- continue;
13516
- if (w.orphanWatchStartAt !== null) {
13517
- const orphanIdle = now - w.orphanWatchStartAt;
13518
- if (orphanIdle >= subagentWaitMs + gracePeriodMs) {
13519
- if (w.resumeAttempts < maxRetries) {
13520
- if (hasInflightTools(w)) {
13521
- await log("debug", `Parent ${short(sid)} has ${w.pendingTools} tool(s) in-flight, skipping orphan-watch abort`);
13522
- w.orphanWatchStartAt = now;
13523
- continue;
13524
- }
13525
- const hasActiveTool = await checkSessionHasActiveTool(sid);
13526
- if (hasActiveTool) {
13527
- await log("debug", `Parent ${short(sid)} has active tool call, skipping orphan-watch abort`);
13528
- w.orphanWatchStartAt = now;
13529
- continue;
13530
- }
13531
- const subStatus = await checkSubagentStatus(sid);
13532
- if (subStatus.status === "crashed" && subStatus.stuckSid) {
13533
- const recovered = await recoverSubagent(subStatus.stuckSid);
13534
- if (recovered) {
13535
- await log("info", `Sent recovery prompt to stuck subagent ${short(subStatus.stuckSid)}, waiting...`);
13536
- } else {
13537
- await log("info", `Subagent crashed, triggering abort+resume on ${short(sid)}`);
13538
- tryAbortAndResume(sid, w);
13516
+ await safe(async () => {
13517
+ const now = Date.now();
13518
+ const numBusy = busyCount();
13519
+ const statusMap = await getSessionStatusMap();
13520
+ for (const [sid, w] of sessions) {
13521
+ const realStatus = statusMap[sid];
13522
+ if (realStatus && realStatus !== w.status) {
13523
+ w.status = realStatus;
13524
+ if (realStatus === "busy")
13525
+ w.idleSince = null;
13526
+ }
13527
+ if (w.status !== "busy")
13528
+ continue;
13529
+ if (w.userCancelled)
13530
+ continue;
13531
+ if (w.aborting)
13532
+ continue;
13533
+ if (w.orphanWatchStartAt !== null) {
13534
+ const orphanIdle = now - w.orphanWatchStartAt;
13535
+ if (orphanIdle >= subagentWaitMs + gracePeriodMs) {
13536
+ if (w.resumeAttempts < maxRetries) {
13537
+ if (hasInflightTools(w)) {
13538
+ await log("debug", `Parent ${short(sid)} has ${w.pendingTools} tool(s) in-flight, skipping orphan-watch abort`);
13539
+ w.orphanWatchStartAt = now;
13540
+ continue;
13541
+ }
13542
+ const hasActiveTool = await checkSessionHasActiveTool(sid);
13543
+ if (hasActiveTool) {
13544
+ await log("debug", `Parent ${short(sid)} has active tool call, skipping orphan-watch abort`);
13545
+ w.orphanWatchStartAt = now;
13546
+ continue;
13539
13547
  }
13540
- } else if (subStatus.status === "idle") {
13541
- const hasBusySub = await hasBusySubagents(sid);
13542
- if (hasBusySub) {
13543
- await log("debug", `Subagents exist but not busy yet, waiting for startup...`);
13548
+ const subStatus = await checkSubagentStatus(sid);
13549
+ if (subStatus.status === "crashed" && subStatus.stuckSid) {
13550
+ const recovered = await recoverSubagent(subStatus.stuckSid);
13551
+ if (recovered) {
13552
+ await log("info", `Sent recovery prompt to stuck subagent ${short(subStatus.stuckSid)}, waiting...`);
13553
+ } else {
13554
+ await log("info", `Subagent crashed, triggering abort+resume on ${short(sid)}`);
13555
+ tryAbortAndResume(sid, w);
13556
+ }
13557
+ } else if (subStatus.status === "idle") {
13558
+ const hasBusySub = await hasBusySubagents(sid);
13559
+ if (hasBusySub) {
13560
+ await log("debug", `Subagents exist but not busy yet, waiting for startup...`);
13561
+ } else {
13562
+ await log("info", `Parent ${short(sid)} stuck with no active subagents. Triggering abort+resume.`);
13563
+ tryAbortAndResume(sid, w);
13564
+ }
13544
13565
  } else {
13545
- await log("info", `Parent ${short(sid)} stuck with no active subagents. Triggering abort+resume.`);
13546
- tryAbortAndResume(sid, w);
13566
+ await log("debug", `Subagent still running, waiting...`);
13547
13567
  }
13548
- } else {
13549
- await log("debug", `Subagent still running, waiting...`);
13568
+ } else if (!w.gaveUp) {
13569
+ w.gaveUp = true;
13570
+ dbg(`State transition on ${short(sid)}: gaveUp=false -> true`);
13571
+ w.orphanWatchStartAt = null;
13572
+ w.aborting = false;
13573
+ log("warn", `${short(sid)} - orphan retries exhausted.`);
13550
13574
  }
13551
- } else if (!w.gaveUp) {
13552
- w.gaveUp = true;
13553
- dbg(`State transition on ${short(sid)}: gaveUp=false -> true`);
13554
- w.orphanWatchStartAt = null;
13555
- w.aborting = false;
13556
- log("warn", `${short(sid)} - orphan retries exhausted.`);
13557
13575
  }
13558
- }
13559
- continue;
13560
- }
13561
- if (numBusy > 1)
13562
- continue;
13563
- if (now - w.lastSubagentCheckAt < checkIntervalMs * 2)
13564
- continue;
13565
- w.lastSubagentCheckAt = now;
13566
- if (w.lastActivityAt > 0 && now - w.lastActivityAt > subagentWaitMs) {
13567
- if (realStatus === "busy") {
13568
- await log("debug", `Session ${short(sid)} is still busy (real status), skipping abort`);
13569
- w.lastSubagentCheckAt = now;
13570
13576
  continue;
13571
13577
  }
13572
- if (hasInflightTools(w)) {
13573
- await log("debug", `Session ${short(sid)} has ${w.pendingTools} tool(s) in-flight, skipping abort check`);
13574
- w.lastSubagentCheckAt = now;
13578
+ if (numBusy > 1)
13575
13579
  continue;
13576
- }
13577
- const hasActiveTool = await checkSessionHasActiveTool(sid);
13578
- if (hasActiveTool) {
13579
- await log("debug", `Session ${short(sid)} has active tool call, skipping abort check`);
13580
- w.lastSubagentCheckAt = now;
13581
- continue;
13582
- }
13583
- const subStatus = await checkSubagentStatus(sid);
13584
- if (subStatus.status === "idle" || subStatus.status === "unknown") {
13585
- await log("info", `Parent ${short(sid)} stuck with no active subagents. Triggering abort+resume.`);
13586
- tryAbortAndResume(sid, w);
13580
+ if (now - w.lastSubagentCheckAt < checkIntervalMs * 2)
13587
13581
  continue;
13588
- } else if (subStatus.status === "crashed" && subStatus.stuckSid) {
13589
- const recovered = await recoverSubagent(subStatus.stuckSid);
13590
- if (recovered) {
13591
- await log("info", `Sent recovery prompt to stuck subagent ${short(subStatus.stuckSid)}, waiting...`);
13592
- } else {
13593
- await log("info", `Parent ${short(sid)} subagent recovery failed. Triggering abort+resume.`);
13594
- tryAbortAndResume(sid, w);
13582
+ w.lastSubagentCheckAt = now;
13583
+ if (w.lastActivityAt > 0 && now - w.lastActivityAt > subagentWaitMs) {
13584
+ if (realStatus === "busy") {
13585
+ await log("debug", `Session ${short(sid)} is still busy (real status), skipping abort`);
13586
+ w.lastSubagentCheckAt = now;
13587
+ continue;
13588
+ }
13589
+ if (hasInflightTools(w)) {
13590
+ await log("debug", `Session ${short(sid)} has ${w.pendingTools} tool(s) in-flight, skipping abort check`);
13591
+ w.lastSubagentCheckAt = now;
13592
+ continue;
13595
13593
  }
13596
- continue;
13597
- }
13598
- }
13599
- const idle = now - w.lastActivityAt;
13600
- if (idle >= chunkTimeoutMs + gracePeriodMs) {
13601
- if (hasInflightTools(w)) {
13602
- await log("debug", `Session ${short(sid)} has ${w.pendingTools} tool(s) in-flight, skipping stall recovery`);
13603
- w.lastSubagentCheckAt = now;
13604
- } else {
13605
13594
  const hasActiveTool = await checkSessionHasActiveTool(sid);
13606
13595
  if (hasActiveTool) {
13607
- await log("debug", `Session ${short(sid)} has active tool call, skipping stall recovery`);
13596
+ await log("debug", `Session ${short(sid)} has active tool call, skipping abort check`);
13608
13597
  w.lastSubagentCheckAt = now;
13609
- } else if (w.resumeAttempts < maxRetries) {
13610
- tryResume(sid, w, "Stream stall");
13611
- } else if (!w.gaveUp) {
13612
- w.gaveUp = true;
13613
- dbg(`State transition on ${short(sid)}: gaveUp=false -> true`);
13614
- log("warn", `${short(sid)} - all ${maxRetries} retries exhausted.`);
13598
+ continue;
13599
+ }
13600
+ const subStatus = await checkSubagentStatus(sid);
13601
+ if (subStatus.status === "idle" || subStatus.status === "unknown") {
13602
+ await log("info", `Parent ${short(sid)} stuck with no active subagents. Triggering abort+resume.`);
13603
+ tryAbortAndResume(sid, w);
13604
+ continue;
13605
+ } else if (subStatus.status === "crashed" && subStatus.stuckSid) {
13606
+ const recovered = await recoverSubagent(subStatus.stuckSid);
13607
+ if (recovered) {
13608
+ await log("info", `Sent recovery prompt to stuck subagent ${short(subStatus.stuckSid)}, waiting...`);
13609
+ } else {
13610
+ await log("info", `Parent ${short(sid)} subagent recovery failed. Triggering abort+resume.`);
13611
+ tryAbortAndResume(sid, w);
13612
+ }
13613
+ continue;
13614
+ }
13615
+ }
13616
+ const idle = now - w.lastActivityAt;
13617
+ if (idle >= chunkTimeoutMs + gracePeriodMs) {
13618
+ if (hasInflightTools(w)) {
13619
+ await log("debug", `Session ${short(sid)} has ${w.pendingTools} tool(s) in-flight, skipping stall recovery`);
13620
+ w.lastSubagentCheckAt = now;
13621
+ } else {
13622
+ const hasActiveTool = await checkSessionHasActiveTool(sid);
13623
+ if (hasActiveTool) {
13624
+ await log("debug", `Session ${short(sid)} has active tool call, skipping stall recovery`);
13625
+ w.lastSubagentCheckAt = now;
13626
+ } else if (w.resumeAttempts < maxRetries) {
13627
+ tryResume(sid, w, "Stream stall");
13628
+ } else if (!w.gaveUp) {
13629
+ w.gaveUp = true;
13630
+ dbg(`State transition on ${short(sid)}: gaveUp=false -> true`);
13631
+ log("warn", `${short(sid)} - all ${maxRetries} retries exhausted.`);
13632
+ }
13615
13633
  }
13616
13634
  }
13617
13635
  }
13618
- }
13619
- for (const [sid, w] of sessions) {
13620
- if (w.pendingRecovery && w.status === "idle" && !w.userCancelled && !w.aborting && !w.continuing && !w.gaveUp && w.recoveryAttempts === 0) {
13621
- dbg(`Pending recovery check on ${short(sid)}: pendingRecovery=${w.pendingRecovery}, status=${w.status}, userCancelled=${w.userCancelled}, aborting=${w.aborting}, continuing=${w.continuing}, gaveUp=${w.gaveUp}, recoveryAttempts=${w.recoveryAttempts}, pendingRecoveryAt=${w.pendingRecoveryAt}`);
13622
- const elapsed = Date.now() - w.pendingRecoveryAt;
13623
- const requiredBackoff2 = backoffMs(w.recoveryAttempts, baseBackoffMs, maxBackoffMs);
13624
- if (elapsed < requiredBackoff2) {
13625
- dbg(`Backoff check on ${short(sid)}: elapsed=${elapsed}ms, required=${requiredBackoff2}ms, attempt=${w.recoveryAttempts}, pass=false`);
13626
- dbg(`Pending recovery on ${short(sid)} waiting for backoff: ${requiredBackoff2 - elapsed}ms remaining`);
13636
+ for (const [sid, w] of sessions) {
13637
+ if (w.pendingRecovery && w.status === "idle" && !w.userCancelled && !w.aborting && !w.continuing && !w.gaveUp && w.recoveryAttempts === 0) {
13638
+ dbg(`Pending recovery check on ${short(sid)}: pendingRecovery=${w.pendingRecovery}, status=${w.status}, userCancelled=${w.userCancelled}, aborting=${w.aborting}, continuing=${w.continuing}, gaveUp=${w.gaveUp}, recoveryAttempts=${w.recoveryAttempts}, pendingRecoveryAt=${w.pendingRecoveryAt}`);
13639
+ const elapsed = Date.now() - w.pendingRecoveryAt;
13640
+ const requiredBackoff2 = backoffMs(w.recoveryAttempts, baseBackoffMs, maxBackoffMs);
13641
+ if (elapsed < requiredBackoff2) {
13642
+ dbg(`Backoff check on ${short(sid)}: elapsed=${elapsed}ms, required=${requiredBackoff2}ms, attempt=${w.recoveryAttempts}, pass=false`);
13643
+ dbg(`Pending recovery on ${short(sid)} waiting for backoff: ${requiredBackoff2 - elapsed}ms remaining`);
13644
+ continue;
13645
+ }
13646
+ dbg(`Backoff check on ${short(sid)}: elapsed=${elapsed}ms, required=${requiredBackoff2}ms, attempt=${w.recoveryAttempts}, pass=true`);
13647
+ await log("info", `Pending recovery triggered on ${short(sid)}: reason=${w.pendingRecoveryReason}, attempt=${w.recoveryAttempts + 1}, maxRetries=${maxRecoveryRetries}`);
13648
+ dbg(`Recovery timing on ${short(sid)}: detectionToAttemptMs=${Date.now() - w.pendingRecoveryAt}`);
13649
+ w.recoveryAttempts++;
13650
+ dbg(`State transition on ${short(sid)}: recoveryAttempts=${w.recoveryAttempts - 1} -> ${w.recoveryAttempts}`);
13651
+ try {
13652
+ await sendContinuePrompt(sid, continuePrompt, w);
13653
+ } catch (err) {
13654
+ const errMsg = err instanceof Error ? err.message : String(err);
13655
+ await log("warn", `${short(sid)} - pending recovery failed: ${errMsg}`);
13656
+ w.recoveryAttempts = 0;
13657
+ }
13658
+ }
13659
+ if (w.status !== "idle")
13660
+ continue;
13661
+ if (w.isSubagent)
13662
+ continue;
13663
+ if (w.userCancelled || w.completionSignaled)
13664
+ continue;
13665
+ if (w.continuing)
13666
+ continue;
13667
+ if (busyCount() !== 0)
13668
+ continue;
13669
+ const open = getOpenTodos(w.todos || []);
13670
+ if (open.length === 0)
13671
+ continue;
13672
+ if (w.todoNudgeAttempts >= maxRetries)
13673
+ continue;
13674
+ const elapsedSinceLastNudge = Date.now() - w.lastRetryAt;
13675
+ const requiredBackoff = backoffMs(w.todoNudgeAttempts, baseBackoffMs, maxBackoffMs);
13676
+ if (w.lastRetryAt > 0 && elapsedSinceLastNudge < requiredBackoff)
13677
+ continue;
13678
+ const isCelebration = await lastAssistantEndsWithCelebration(sid);
13679
+ if (isCelebration) {
13680
+ w.toolTextRecovered = true;
13681
+ w.completionSignaled = true;
13627
13682
  continue;
13628
13683
  }
13629
- dbg(`Backoff check on ${short(sid)}: elapsed=${elapsed}ms, required=${requiredBackoff2}ms, attempt=${w.recoveryAttempts}, pass=true`);
13630
- await log("info", `Pending recovery triggered on ${short(sid)}: reason=${w.pendingRecoveryReason}, attempt=${w.recoveryAttempts + 1}, maxRetries=${maxRecoveryRetries}`);
13631
- dbg(`Recovery timing on ${short(sid)}: detectionToAttemptMs=${Date.now() - w.pendingRecoveryAt}`);
13632
- w.recoveryAttempts++;
13633
- dbg(`State transition on ${short(sid)}: recoveryAttempts=${w.recoveryAttempts - 1} -> ${w.recoveryAttempts}`);
13634
- try {
13635
- await sendContinuePrompt(sid, continuePrompt, w);
13636
- } catch (err) {
13637
- const errMsg = err instanceof Error ? err.message : String(err);
13638
- await log("warn", `${short(sid)} - pending recovery failed: ${errMsg}`);
13639
- w.recoveryAttempts = 0;
13684
+ const reminder = buildOpenTodosReminder(w.todos || []);
13685
+ const sent = await tryResume(sid, w, "Idle with open todos (periodic)", reminder);
13686
+ if (sent) {
13687
+ w.todoNudgeAttempts++;
13688
+ await log("info", `${short(sid)} - idle periodic recheck: nudge ${w.todoNudgeAttempts}/${maxRetries}`);
13640
13689
  }
13641
13690
  }
13642
- if (w.status !== "idle")
13643
- continue;
13644
- if (w.isSubagent)
13645
- continue;
13646
- if (w.userCancelled || w.completionSignaled)
13647
- continue;
13648
- if (w.continuing)
13649
- continue;
13650
- if (busyCount() !== 0)
13651
- continue;
13652
- const open = getOpenTodos(w.todos || []);
13653
- if (open.length === 0)
13654
- continue;
13655
- if (w.todoNudgeAttempts >= maxRetries)
13656
- continue;
13657
- const elapsedSinceLastNudge = Date.now() - w.lastRetryAt;
13658
- const requiredBackoff = backoffMs(w.todoNudgeAttempts, baseBackoffMs, maxBackoffMs);
13659
- if (w.lastRetryAt > 0 && elapsedSinceLastNudge < requiredBackoff)
13660
- continue;
13661
- const isCelebration = await lastAssistantEndsWithCelebration(sid);
13662
- if (isCelebration) {
13663
- w.toolTextRecovered = true;
13664
- w.completionSignaled = true;
13665
- continue;
13666
- }
13667
- const reminder = buildOpenTodosReminder(w.todos || []);
13668
- const sent = await tryResume(sid, w, "Idle with open todos (periodic)", reminder);
13669
- if (sent) {
13670
- w.todoNudgeAttempts++;
13671
- await log("info", `${short(sid)} - idle periodic recheck: nudge ${w.todoNudgeAttempts}/${maxRetries}`);
13672
- }
13673
- }
13674
- cleanupIdleSessions();
13691
+ cleanupIdleSessions();
13692
+ }, "periodic timer");
13675
13693
  }, checkIntervalMs);
13676
13694
  if (timer.unref)
13677
13695
  timer.unref();
13678
13696
  discoveryTimer = setInterval(() => {
13679
- discoverSessions();
13697
+ safe(discoverSessions, "discoveryTimer").catch(() => {});
13680
13698
  }, SESSION_DISCOVERY_INTERVAL_MS);
13681
13699
  if (discoveryTimer.unref)
13682
13700
  discoveryTimer.unref();
@@ -13895,12 +13913,13 @@ var AutoResumePlugin = async (ctx, options) => {
13895
13913
  if (!sid)
13896
13914
  break;
13897
13915
  const props = ev.properties;
13898
- const todos = props?.todos ?? [];
13916
+ const rawTodos = props?.todos;
13917
+ const todos = Array.isArray(rawTodos) ? rawTodos : [];
13899
13918
  const w = ensureWatch(sid);
13900
13919
  w.todos = todos.map((t) => ({
13901
- content: t.content ?? "",
13902
- status: t.status ?? "pending",
13903
- priority: t.priority ?? "medium"
13920
+ content: t?.content ?? "",
13921
+ status: t?.status ?? "pending",
13922
+ priority: t?.priority ?? "medium"
13904
13923
  }));
13905
13924
  break;
13906
13925
  }
@@ -13997,7 +14016,11 @@ var AutoResumePlugin = async (ctx, options) => {
13997
14016
  initialised = true;
13998
14017
  log("info", `opencode-auto-resume ready. timeout=${chunkTimeoutMs}ms, orphan=${subagentWaitMs}ms, loop=${loopMaxContinues}x/${loopWindowMs / 1000}s`);
13999
14018
  }
14000
- handleEvent(event);
14019
+ handleEvent(event).catch((e) => {
14020
+ const msg = e instanceof Error ? e.message : String(e);
14021
+ console.error(`[auto-resume] handleEvent error: ${msg}`);
14022
+ log("error", `handleEvent error: ${msg}`).catch(() => {});
14023
+ });
14001
14024
  },
14002
14025
  config: async () => {
14003
14026
  log("info", `opencode-auto-resume config OK`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-auto-resume",
3
- "version": "1.1.5",
3
+ "version": "1.1.8",
4
4
  "description": "OpenCode plugin that automatically resumes stalled LLM sessions when thinking/streaming freezes mid-generation.",
5
5
  "keywords": [
6
6
  "opencode",