opencode-auto-resume 1.0.5 → 1.0.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.
package/README.md CHANGED
@@ -7,6 +7,8 @@
7
7
  LLM sessions fail in predictable ways. This plugin monitors all sessions and automatically recovers without user intervention.
8
8
 
9
9
  **Stall recovery** — the stream goes silent but the session stays "busy". The UI shows a blinking cursor with no progress. If no events arrive for 48 seconds, the plugin sends `"continue"` with exponential backoff. After 3 failed attempts it gives up.
10
+
11
+ The plugin extracts the **agent, model, and provider** from the last session message, so it resumes with the exact same configuration the user was using (build, sisyphus, prometheus, etc.).
10
12
  _( [#55](https://github.com/opencode-ai/opencode/issues/55), [#199](https://github.com/opencode-ai/opencode/issues/199), [#283](https://github.com/opencode-ai/opencode/issues/283) )_
11
13
 
12
14
  **Tool calls as raw text** — the model prints tool invocations as raw XML (`<function=edit>...`) instead of executing them. The session goes idle normally but the tool was never run. On idle, the plugin fetches the last messages and scans for XML tool-call patterns (including truncated and alternative formats). If found, it sends a specific recovery prompt.
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ var MAX_IDLE_SESSIONS = 50;
15
15
  var IDLE_CLEANUP_MS = 10 * 60000;
16
16
  var SESSION_DISCOVERY_INTERVAL_MS = 60000;
17
17
  var TOOL_TEXT_RECOVERY_PROMPT = "Your last message contained a raw tool call printed as text instead of being executed. " + "Please use the proper tool calling mechanism to execute it.";
18
+ var THINKING_TOOL_RECOVERY_PROMPT = "I noticed you have a tool call generated in your thinking/reasoning. " + "Please execute it using the proper tool calling mechanism instead of keeping it in reasoning.";
18
19
  var TOOL_TEXT_PATTERNS = [
19
20
  /<function\s*=/i,
20
21
  /<function>/i,
@@ -210,12 +211,45 @@ var AutoResumePlugin = async (ctx, options) => {
210
211
  }
211
212
  w.continuing = true;
212
213
  try {
214
+ let agent;
215
+ let modelID;
216
+ let providerID;
217
+ const msgResp = await ctx.client.session.messages({ path: { id: sid } });
218
+ const msgs = extractMessages(msgResp);
219
+ const lastMsg = msgs[msgs.length - 1];
220
+ if (lastMsg) {
221
+ const info = lastMsg.info;
222
+ agent = info?.agent;
223
+ if (lastMsg.role === "assistant") {
224
+ modelID = lastMsg.modelID;
225
+ providerID = lastMsg.providerID;
226
+ }
227
+ }
213
228
  await ctx.client.session.prompt({
214
229
  path: { id: sid },
215
- body: { parts: [{ type: "text", text }] }
230
+ body: { parts: [{ type: "text", text }] },
231
+ agent,
232
+ modelID,
233
+ providerID
216
234
  });
235
+ await log("debug", `${short(sid)} - prompt sent with agent: ${agent}, model: ${modelID}`);
217
236
  recordContinue(sid);
218
237
  w.lastRetryAt = Date.now();
238
+ } catch (err) {
239
+ const errMsg = err instanceof Error ? err.message : String(err);
240
+ await log("warn", `${short(sid)} - prompt failed: ${errMsg}`);
241
+ try {
242
+ await ctx.client.session.prompt({
243
+ path: { id: sid },
244
+ body: { parts: [{ type: "text", text }] }
245
+ });
246
+ recordContinue(sid);
247
+ w.lastRetryAt = Date.now();
248
+ } catch (retryErr) {
249
+ const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
250
+ await log("error", `${short(sid)} - prompt retry also failed: ${retryMsg}`);
251
+ throw retryErr;
252
+ }
219
253
  } finally {
220
254
  w.continuing = false;
221
255
  }
@@ -229,6 +263,33 @@ var AutoResumePlugin = async (ctx, options) => {
229
263
  return response.messages;
230
264
  return [];
231
265
  }
266
+ async function checkSubagentCrashed(parentSid) {
267
+ try {
268
+ const response = await ctx.client.session.list();
269
+ const sessions2 = extractMessages(response);
270
+ for (const s of sessions2) {
271
+ const sId = s.id;
272
+ if (!sId || sId === parentSid)
273
+ continue;
274
+ const status = s.status;
275
+ if (status === "busy") {
276
+ const msgResponse = await ctx.client.session.messages({ path: { id: sId } });
277
+ const messages = extractMessages(msgResponse);
278
+ const lastMsg = messages[messages.length - 1];
279
+ if (lastMsg && lastMsg.role === "assistant" && "error" in lastMsg) {
280
+ const error = lastMsg.error;
281
+ const errorName = error?.name;
282
+ await log("debug", `Subagent ${short(sId)} appears crashed: ${errorName}`);
283
+ return true;
284
+ }
285
+ }
286
+ }
287
+ } catch (err) {
288
+ const errMsg = err instanceof Error ? err.message : String(err);
289
+ await log("debug", `checkSubagentCrashed failed: ${errMsg}`);
290
+ }
291
+ return false;
292
+ }
232
293
  function resetSessionFlags(w) {
233
294
  w.userCancelled = false;
234
295
  w.resumeAttempts = 0;
@@ -275,19 +336,29 @@ var AutoResumePlugin = async (ctx, options) => {
275
336
  if (!parts)
276
337
  continue;
277
338
  for (const part of parts) {
278
- if (part.type !== "text")
339
+ const partType = part.type;
340
+ let text = "";
341
+ let isReasoning = false;
342
+ if (partType === "text") {
343
+ text = part.text ?? "";
344
+ } else if (partType === "reasoning") {
345
+ text = part.text ?? "";
346
+ isReasoning = true;
347
+ } else {
279
348
  continue;
280
- const text = part.text ?? "";
349
+ }
281
350
  if (containsToolCallAsText(text)) {
282
351
  w.toolTextRecovered = true;
283
352
  w.toolTextAttempts++;
284
- await log("info", `Tool-call-as-text detected on ${short(sid)}! ` + `Attempt ${w.toolTextAttempts}/${maxRetries}. Sending recovery prompt...`);
353
+ const prompt = isReasoning ? THINKING_TOOL_RECOVERY_PROMPT : TOOL_TEXT_RECOVERY_PROMPT;
354
+ const source = isReasoning ? "reasoning" : "text";
355
+ await log("info", `Tool-call-as-text in ${source} detected on ${short(sid)}! ` + `Attempt ${w.toolTextAttempts}/${maxRetries}. Sending recovery prompt...`);
285
356
  if (isHallucinationLoop(sid)) {
286
357
  await log("warn", `Hallucination loop detected on ${short(sid)} \u2014 aborting instead`);
287
358
  await tryAbortAndResume(sid, w);
288
359
  } else {
289
360
  try {
290
- await sendContinuePrompt(sid, TOOL_TEXT_RECOVERY_PROMPT, w);
361
+ await sendContinuePrompt(sid, prompt, w);
291
362
  await log("info", `${short(sid)} - tool-call-as-text recovery sent (attempt ${w.toolTextAttempts})`);
292
363
  } catch (err) {
293
364
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -412,7 +483,7 @@ var AutoResumePlugin = async (ctx, options) => {
412
483
  function startTimer() {
413
484
  if (timer)
414
485
  return;
415
- timer = setInterval(() => {
486
+ timer = setInterval(async () => {
416
487
  const now = Date.now();
417
488
  const numBusy = busyCount();
418
489
  for (const [sid, w] of sessions) {
@@ -426,7 +497,13 @@ var AutoResumePlugin = async (ctx, options) => {
426
497
  const orphanIdle = now - w.orphanWatchStartAt;
427
498
  if (orphanIdle >= subagentWaitMs + gracePeriodMs) {
428
499
  if (w.resumeAttempts < maxRetries) {
429
- tryAbortAndResume(sid, w);
500
+ const crashed = await checkSubagentCrashed(sid);
501
+ if (crashed) {
502
+ await log("info", `Subagent crashed, triggering abort+resume on ${short(sid)}`);
503
+ tryAbortAndResume(sid, w);
504
+ } else {
505
+ await log("debug", `Subagent still running, waiting...`);
506
+ }
430
507
  } else if (!w.gaveUp) {
431
508
  w.gaveUp = true;
432
509
  w.orphanWatchStartAt = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-auto-resume",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "OpenCode plugin that automatically resumes stalled LLM sessions when thinking/streaming freezes mid-generation.",
5
5
  "keywords": [
6
6
  "opencode",