opencode-auto-resume 1.0.7 → 1.0.9

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 +6 -0
  2. package/dist/index.js +68 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,6 +30,12 @@ _( [#128](https://github.com/opencode-ai/opencode/pulls/128), [#22](https://gith
30
30
 
31
31
  **Session discovery** — periodically calls `session.list()` to pick up sessions that were missed by event tracking. Idle sessions are cleaned up after 10 minutes to prevent memory leaks.
32
32
 
33
+ **Model preservation** — when resuming with "continue", the plugin extracts agent, model and provider from the last session message (not from `info` field) to preserve the user's UI selection.
34
+ _( [#111](https://github.com/opencode-ai/opencode/issues/111), [#277](https://github.com/opencode-ai/opencode/issues/277) )_
35
+
36
+ **Subagent stuck detection** — detects when a subagent hasn't received new text for >1 minute (or >3 minutes if a tool call is in progress). If stuck, sends a recovery prompt before triggering abort+resume on the parent.
37
+ _( [#55](https://github.com/opencode-ai/opencode/issues/55), [#60](https://github.com/opencode-ai/opencode/issues/60), [#246](https://github.com/opencode-ai/opencode/issues/246) )_
38
+
33
39
  ## Architecture
34
40
 
35
41
  ```
package/dist/index.js CHANGED
@@ -217,12 +217,15 @@ var AutoResumePlugin = async (ctx, options) => {
217
217
  const msgs = extractMessages(msgResp);
218
218
  for (let i = msgs.length - 1;i >= 0; i--) {
219
219
  const msg = msgs[i];
220
- const info = msg.info;
221
- if (info?.role === "user") {
222
- const rawAgent = info.agent;
220
+ const role = msg.role ?? msg.info?.role;
221
+ if (role === "user") {
222
+ const rawAgent = msg.agent;
223
223
  if (typeof rawAgent === "string")
224
224
  agent2 = rawAgent;
225
- const rawModel = info.model;
225
+ let rawModel = msg.model;
226
+ if (!rawModel) {
227
+ rawModel = msg.info?.model;
228
+ }
226
229
  if (rawModel && typeof rawModel.providerID === "string" && typeof rawModel.modelID === "string") {
227
230
  model2 = {
228
231
  providerID: rawModel.providerID,
@@ -271,10 +274,27 @@ var AutoResumePlugin = async (ctx, options) => {
271
274
  return response.messages;
272
275
  return [];
273
276
  }
277
+ const SUBAGENT_STUCK_MS = 60000;
278
+ const SUBAGENT_RECOVERY_PROMPT = "It looks like you may have stalled or timed out. Please retry the last operation or continue with the task.";
279
+ async function recoverSubagent(subagentSid) {
280
+ try {
281
+ await ctx.client.session.prompt({
282
+ path: { id: subagentSid },
283
+ body: { parts: [{ type: "text", text: SUBAGENT_RECOVERY_PROMPT }] }
284
+ });
285
+ await log("info", `Sent recovery prompt to subagent ${short(subagentSid)}`);
286
+ return true;
287
+ } catch (err) {
288
+ const errMsg = err instanceof Error ? err.message : String(err);
289
+ await log("warn", `Failed to recover subagent ${short(subagentSid)}: ${errMsg}`);
290
+ return false;
291
+ }
292
+ }
274
293
  async function checkSubagentStatus(parentSid) {
275
294
  try {
276
295
  const response = await ctx.client.session.list();
277
296
  const allSessions = extractMessages(response);
297
+ const now = Date.now();
278
298
  let hasBusySubagent = false;
279
299
  for (const s of allSessions) {
280
300
  const sId = s.id;
@@ -291,16 +311,23 @@ var AutoResumePlugin = async (ctx, options) => {
291
311
  await log("debug", `Subagent ${short(sId)} appears crashed`);
292
312
  return "crashed";
293
313
  }
314
+ const msgTime = lastMsg?.time?.created ?? lastMsg?.time;
315
+ const hasToolCall = lastMsg?.toolCall !== undefined || lastMsg?.tool_calls !== undefined || lastMsg?.parts?.some((p) => p.type === "tool-call") !== undefined;
316
+ const isStuck = hasToolCall ? now - msgTime > SUBAGENT_STUCK_MS * 3 : now - msgTime > SUBAGENT_STUCK_MS;
317
+ if (isStuck) {
318
+ await log("debug", `Subagent ${short(sId)} stuck - no new text in >${hasToolCall ? 3 : 1}min`);
319
+ return { status: "crashed", stuckSid: sId };
320
+ }
294
321
  }
295
322
  }
296
323
  if (!hasBusySubagent) {
297
- return "idle";
324
+ return { status: "idle" };
298
325
  }
299
- return "busy";
326
+ return { status: "busy" };
300
327
  } catch (err) {
301
328
  const errMsg = err instanceof Error ? err.message : String(err);
302
329
  await log("debug", `checkSubagentStatus failed: ${errMsg}`);
303
- return "unknown";
330
+ return { status: "unknown" };
304
331
  }
305
332
  }
306
333
  function resetSessionFlags(w) {
@@ -511,10 +538,15 @@ var AutoResumePlugin = async (ctx, options) => {
511
538
  if (orphanIdle >= subagentWaitMs + gracePeriodMs) {
512
539
  if (w.resumeAttempts < maxRetries) {
513
540
  const subStatus = await checkSubagentStatus(sid);
514
- if (subStatus === "crashed") {
515
- await log("info", `Subagent crashed, triggering abort+resume on ${short(sid)}`);
516
- tryAbortAndResume(sid, w);
517
- } else if (subStatus === "idle") {
541
+ if (subStatus.status === "crashed" && subStatus.stuckSid) {
542
+ const recovered = await recoverSubagent(subStatus.stuckSid);
543
+ if (recovered) {
544
+ await log("info", `Sent recovery prompt to stuck subagent ${short(subStatus.stuckSid)}, waiting...`);
545
+ } else {
546
+ await log("info", `Subagent crashed, triggering abort+resume on ${short(sid)}`);
547
+ tryAbortAndResume(sid, w);
548
+ }
549
+ } else if (subStatus.status === "idle") {
518
550
  await log("info", `All subagents idle, parent ${short(sid)} stuck. Triggering abort+resume.`);
519
551
  tryAbortAndResume(sid, w);
520
552
  } else {
@@ -531,6 +563,23 @@ var AutoResumePlugin = async (ctx, options) => {
531
563
  }
532
564
  if (numBusy > 1)
533
565
  continue;
566
+ if (w.lastActivityAt > 0 && now - w.lastActivityAt > subagentWaitMs) {
567
+ const subStatus = await checkSubagentStatus(sid);
568
+ if (subStatus.status === "idle" || subStatus.status === "unknown") {
569
+ await log("info", `Parent ${short(sid)} stuck with no active subagents. Triggering abort+resume.`);
570
+ tryAbortAndResume(sid, w);
571
+ continue;
572
+ } else if (subStatus.status === "crashed" && subStatus.stuckSid) {
573
+ const recovered = await recoverSubagent(subStatus.stuckSid);
574
+ if (recovered) {
575
+ await log("info", `Sent recovery prompt to stuck subagent ${short(subStatus.stuckSid)}, waiting...`);
576
+ } else {
577
+ await log("info", `Parent ${short(sid)} subagent recovery failed. Triggering abort+resume.`);
578
+ tryAbortAndResume(sid, w);
579
+ }
580
+ continue;
581
+ }
582
+ }
534
583
  const idle = now - w.lastActivityAt;
535
584
  if (idle >= chunkTimeoutMs + gracePeriodMs) {
536
585
  if (w.resumeAttempts < maxRetries) {
@@ -661,12 +710,14 @@ var AutoResumePlugin = async (ctx, options) => {
661
710
  },
662
711
  config: async () => {
663
712
  log("info", `opencode-auto-resume config OK`);
664
- ctx.ui.toast({
665
- title: "Auto-Resume Plugin",
666
- message: `Loaded with ${chunkTimeoutMs}ms timeout, ${loopMaxContinues} loop attempts`,
667
- variant: "success",
668
- duration: 5000
669
- });
713
+ if (ctx.ui && typeof ctx.ui.toast === "function") {
714
+ ctx.ui.toast({
715
+ title: "Auto-Resume Plugin",
716
+ message: `Loaded with ${chunkTimeoutMs}ms timeout, ${loopMaxContinues} loop attempts`,
717
+ variant: "success",
718
+ duration: 5000
719
+ });
720
+ }
670
721
  }
671
722
  };
672
723
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-auto-resume",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "OpenCode plugin that automatically resumes stalled LLM sessions when thinking/streaming freezes mid-generation.",
5
5
  "keywords": [
6
6
  "opencode",