opencode-auto-resume 1.1.5 → 1.1.6

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 +178 -158
  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
@@ -12593,7 +12593,21 @@ var AutoResumePlugin = async (ctx, options) => {
12593
12593
  async function log(level, msg) {
12594
12594
  try {
12595
12595
  await ctx.client.app.log({ body: { service: "auto-resume", level, message: msg } });
12596
- } catch {}
12596
+ } catch (e) {
12597
+ console.error("[auto-resume] log() failed:", e instanceof Error ? e.message : String(e));
12598
+ }
12599
+ }
12600
+ async function safe(fn, ctxLabel) {
12601
+ try {
12602
+ return await fn();
12603
+ } catch (e) {
12604
+ const msg = e instanceof Error ? e.message : String(e);
12605
+ console.error(`[auto-resume] ${ctxLabel}: ${msg}`);
12606
+ try {
12607
+ await log("error", `${ctxLabel}: ${msg}`);
12608
+ } catch {}
12609
+ return;
12610
+ }
12597
12611
  }
12598
12612
  function ensureWatch(sid) {
12599
12613
  let w = sessions.get(sid);
@@ -13497,186 +13511,188 @@ var AutoResumePlugin = async (ctx, options) => {
13497
13511
  if (timer)
13498
13512
  return;
13499
13513
  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);
13514
+ await safe(async () => {
13515
+ const now = Date.now();
13516
+ const numBusy = busyCount();
13517
+ const statusMap = await getSessionStatusMap();
13518
+ for (const [sid, w] of sessions) {
13519
+ const realStatus = statusMap[sid];
13520
+ if (realStatus && realStatus !== w.status) {
13521
+ w.status = realStatus;
13522
+ if (realStatus === "busy")
13523
+ w.idleSince = null;
13524
+ }
13525
+ if (w.status !== "busy")
13526
+ continue;
13527
+ if (w.userCancelled)
13528
+ continue;
13529
+ if (w.aborting)
13530
+ continue;
13531
+ if (w.orphanWatchStartAt !== null) {
13532
+ const orphanIdle = now - w.orphanWatchStartAt;
13533
+ if (orphanIdle >= subagentWaitMs + gracePeriodMs) {
13534
+ if (w.resumeAttempts < maxRetries) {
13535
+ if (hasInflightTools(w)) {
13536
+ await log("debug", `Parent ${short(sid)} has ${w.pendingTools} tool(s) in-flight, skipping orphan-watch abort`);
13537
+ w.orphanWatchStartAt = now;
13538
+ continue;
13539
13539
  }
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...`);
13540
+ const hasActiveTool = await checkSessionHasActiveTool(sid);
13541
+ if (hasActiveTool) {
13542
+ await log("debug", `Parent ${short(sid)} has active tool call, skipping orphan-watch abort`);
13543
+ w.orphanWatchStartAt = now;
13544
+ continue;
13545
+ }
13546
+ const subStatus = await checkSubagentStatus(sid);
13547
+ if (subStatus.status === "crashed" && subStatus.stuckSid) {
13548
+ const recovered = await recoverSubagent(subStatus.stuckSid);
13549
+ if (recovered) {
13550
+ await log("info", `Sent recovery prompt to stuck subagent ${short(subStatus.stuckSid)}, waiting...`);
13551
+ } else {
13552
+ await log("info", `Subagent crashed, triggering abort+resume on ${short(sid)}`);
13553
+ tryAbortAndResume(sid, w);
13554
+ }
13555
+ } else if (subStatus.status === "idle") {
13556
+ const hasBusySub = await hasBusySubagents(sid);
13557
+ if (hasBusySub) {
13558
+ await log("debug", `Subagents exist but not busy yet, waiting for startup...`);
13559
+ } else {
13560
+ await log("info", `Parent ${short(sid)} stuck with no active subagents. Triggering abort+resume.`);
13561
+ tryAbortAndResume(sid, w);
13562
+ }
13544
13563
  } else {
13545
- await log("info", `Parent ${short(sid)} stuck with no active subagents. Triggering abort+resume.`);
13546
- tryAbortAndResume(sid, w);
13564
+ await log("debug", `Subagent still running, waiting...`);
13547
13565
  }
13548
- } else {
13549
- await log("debug", `Subagent still running, waiting...`);
13566
+ } else if (!w.gaveUp) {
13567
+ w.gaveUp = true;
13568
+ dbg(`State transition on ${short(sid)}: gaveUp=false -> true`);
13569
+ w.orphanWatchStartAt = null;
13570
+ w.aborting = false;
13571
+ log("warn", `${short(sid)} - orphan retries exhausted.`);
13550
13572
  }
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
13573
  }
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
- continue;
13571
- }
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;
13575
13574
  continue;
13576
13575
  }
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;
13576
+ if (numBusy > 1)
13581
13577
  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);
13578
+ if (now - w.lastSubagentCheckAt < checkIntervalMs * 2)
13587
13579
  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);
13580
+ w.lastSubagentCheckAt = now;
13581
+ if (w.lastActivityAt > 0 && now - w.lastActivityAt > subagentWaitMs) {
13582
+ if (realStatus === "busy") {
13583
+ await log("debug", `Session ${short(sid)} is still busy (real status), skipping abort`);
13584
+ w.lastSubagentCheckAt = now;
13585
+ continue;
13586
+ }
13587
+ if (hasInflightTools(w)) {
13588
+ await log("debug", `Session ${short(sid)} has ${w.pendingTools} tool(s) in-flight, skipping abort check`);
13589
+ w.lastSubagentCheckAt = now;
13590
+ continue;
13595
13591
  }
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
13592
  const hasActiveTool = await checkSessionHasActiveTool(sid);
13606
13593
  if (hasActiveTool) {
13607
- await log("debug", `Session ${short(sid)} has active tool call, skipping stall recovery`);
13594
+ await log("debug", `Session ${short(sid)} has active tool call, skipping abort check`);
13608
13595
  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.`);
13596
+ continue;
13597
+ }
13598
+ const subStatus = await checkSubagentStatus(sid);
13599
+ if (subStatus.status === "idle" || subStatus.status === "unknown") {
13600
+ await log("info", `Parent ${short(sid)} stuck with no active subagents. Triggering abort+resume.`);
13601
+ tryAbortAndResume(sid, w);
13602
+ continue;
13603
+ } else if (subStatus.status === "crashed" && subStatus.stuckSid) {
13604
+ const recovered = await recoverSubagent(subStatus.stuckSid);
13605
+ if (recovered) {
13606
+ await log("info", `Sent recovery prompt to stuck subagent ${short(subStatus.stuckSid)}, waiting...`);
13607
+ } else {
13608
+ await log("info", `Parent ${short(sid)} subagent recovery failed. Triggering abort+resume.`);
13609
+ tryAbortAndResume(sid, w);
13610
+ }
13611
+ continue;
13612
+ }
13613
+ }
13614
+ const idle = now - w.lastActivityAt;
13615
+ if (idle >= chunkTimeoutMs + gracePeriodMs) {
13616
+ if (hasInflightTools(w)) {
13617
+ await log("debug", `Session ${short(sid)} has ${w.pendingTools} tool(s) in-flight, skipping stall recovery`);
13618
+ w.lastSubagentCheckAt = now;
13619
+ } else {
13620
+ const hasActiveTool = await checkSessionHasActiveTool(sid);
13621
+ if (hasActiveTool) {
13622
+ await log("debug", `Session ${short(sid)} has active tool call, skipping stall recovery`);
13623
+ w.lastSubagentCheckAt = now;
13624
+ } else if (w.resumeAttempts < maxRetries) {
13625
+ tryResume(sid, w, "Stream stall");
13626
+ } else if (!w.gaveUp) {
13627
+ w.gaveUp = true;
13628
+ dbg(`State transition on ${short(sid)}: gaveUp=false -> true`);
13629
+ log("warn", `${short(sid)} - all ${maxRetries} retries exhausted.`);
13630
+ }
13615
13631
  }
13616
13632
  }
13617
13633
  }
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`);
13634
+ for (const [sid, w] of sessions) {
13635
+ if (w.pendingRecovery && w.status === "idle" && !w.userCancelled && !w.aborting && !w.continuing && !w.gaveUp && w.recoveryAttempts === 0) {
13636
+ 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}`);
13637
+ const elapsed = Date.now() - w.pendingRecoveryAt;
13638
+ const requiredBackoff2 = backoffMs(w.recoveryAttempts, baseBackoffMs, maxBackoffMs);
13639
+ if (elapsed < requiredBackoff2) {
13640
+ dbg(`Backoff check on ${short(sid)}: elapsed=${elapsed}ms, required=${requiredBackoff2}ms, attempt=${w.recoveryAttempts}, pass=false`);
13641
+ dbg(`Pending recovery on ${short(sid)} waiting for backoff: ${requiredBackoff2 - elapsed}ms remaining`);
13642
+ continue;
13643
+ }
13644
+ dbg(`Backoff check on ${short(sid)}: elapsed=${elapsed}ms, required=${requiredBackoff2}ms, attempt=${w.recoveryAttempts}, pass=true`);
13645
+ await log("info", `Pending recovery triggered on ${short(sid)}: reason=${w.pendingRecoveryReason}, attempt=${w.recoveryAttempts + 1}, maxRetries=${maxRecoveryRetries}`);
13646
+ dbg(`Recovery timing on ${short(sid)}: detectionToAttemptMs=${Date.now() - w.pendingRecoveryAt}`);
13647
+ w.recoveryAttempts++;
13648
+ dbg(`State transition on ${short(sid)}: recoveryAttempts=${w.recoveryAttempts - 1} -> ${w.recoveryAttempts}`);
13649
+ try {
13650
+ await sendContinuePrompt(sid, continuePrompt, w);
13651
+ } catch (err) {
13652
+ const errMsg = err instanceof Error ? err.message : String(err);
13653
+ await log("warn", `${short(sid)} - pending recovery failed: ${errMsg}`);
13654
+ w.recoveryAttempts = 0;
13655
+ }
13656
+ }
13657
+ if (w.status !== "idle")
13658
+ continue;
13659
+ if (w.isSubagent)
13660
+ continue;
13661
+ if (w.userCancelled || w.completionSignaled)
13662
+ continue;
13663
+ if (w.continuing)
13664
+ continue;
13665
+ if (busyCount() !== 0)
13666
+ continue;
13667
+ const open = getOpenTodos(w.todos || []);
13668
+ if (open.length === 0)
13669
+ continue;
13670
+ if (w.todoNudgeAttempts >= maxRetries)
13671
+ continue;
13672
+ const elapsedSinceLastNudge = Date.now() - w.lastRetryAt;
13673
+ const requiredBackoff = backoffMs(w.todoNudgeAttempts, baseBackoffMs, maxBackoffMs);
13674
+ if (w.lastRetryAt > 0 && elapsedSinceLastNudge < requiredBackoff)
13675
+ continue;
13676
+ const isCelebration = await lastAssistantEndsWithCelebration(sid);
13677
+ if (isCelebration) {
13678
+ w.toolTextRecovered = true;
13679
+ w.completionSignaled = true;
13627
13680
  continue;
13628
13681
  }
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;
13682
+ const reminder = buildOpenTodosReminder(w.todos || []);
13683
+ const sent = await tryResume(sid, w, "Idle with open todos (periodic)", reminder);
13684
+ if (sent) {
13685
+ w.todoNudgeAttempts++;
13686
+ await log("info", `${short(sid)} - idle periodic recheck: nudge ${w.todoNudgeAttempts}/${maxRetries}`);
13640
13687
  }
13641
13688
  }
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();
13689
+ cleanupIdleSessions();
13690
+ }, "periodic timer");
13675
13691
  }, checkIntervalMs);
13676
13692
  if (timer.unref)
13677
13693
  timer.unref();
13678
13694
  discoveryTimer = setInterval(() => {
13679
- discoverSessions();
13695
+ safe(discoverSessions, "discoveryTimer").catch(() => {});
13680
13696
  }, SESSION_DISCOVERY_INTERVAL_MS);
13681
13697
  if (discoveryTimer.unref)
13682
13698
  discoveryTimer.unref();
@@ -13997,7 +14013,11 @@ var AutoResumePlugin = async (ctx, options) => {
13997
14013
  initialised = true;
13998
14014
  log("info", `opencode-auto-resume ready. timeout=${chunkTimeoutMs}ms, orphan=${subagentWaitMs}ms, loop=${loopMaxContinues}x/${loopWindowMs / 1000}s`);
13999
14015
  }
14000
- handleEvent(event);
14016
+ handleEvent(event).catch((e) => {
14017
+ const msg = e instanceof Error ? e.message : String(e);
14018
+ console.error(`[auto-resume] handleEvent error: ${msg}`);
14019
+ log("error", `handleEvent error: ${msg}`).catch(() => {});
14020
+ });
14001
14021
  },
14002
14022
  config: async () => {
14003
14023
  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.6",
4
4
  "description": "OpenCode plugin that automatically resumes stalled LLM sessions when thinking/streaming freezes mid-generation.",
5
5
  "keywords": [
6
6
  "opencode",