opencode-auto-resume 1.0.6 → 1.0.7

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 +78 -62
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -211,28 +211,36 @@ var AutoResumePlugin = async (ctx, options) => {
211
211
  }
212
212
  w.continuing = true;
213
213
  try {
214
- let agent;
215
- let modelID;
216
- let providerID;
214
+ let agent2;
215
+ let model2;
217
216
  const msgResp = await ctx.client.session.messages({ path: { id: sid } });
218
217
  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;
218
+ for (let i = msgs.length - 1;i >= 0; i--) {
219
+ const msg = msgs[i];
220
+ const info = msg.info;
221
+ if (info?.role === "user") {
222
+ const rawAgent = info.agent;
223
+ if (typeof rawAgent === "string")
224
+ agent2 = rawAgent;
225
+ const rawModel = info.model;
226
+ if (rawModel && typeof rawModel.providerID === "string" && typeof rawModel.modelID === "string") {
227
+ model2 = {
228
+ providerID: rawModel.providerID,
229
+ modelID: rawModel.modelID
230
+ };
231
+ }
232
+ break;
226
233
  }
227
234
  }
228
235
  await ctx.client.session.prompt({
229
236
  path: { id: sid },
230
- body: { parts: [{ type: "text", text }] },
231
- agent,
232
- modelID,
233
- providerID
237
+ body: {
238
+ parts: [{ type: "text", text }],
239
+ agent: agent2,
240
+ model: model2
241
+ }
234
242
  });
235
- await log("debug", `${short(sid)} - prompt sent with agent: ${agent}, model: ${modelID}`);
243
+ await log("debug", `${short(sid)} - prompt sent with agent: ${agent2 ?? "(default)"}, model: ${model2 ? `${model2.providerID}/${model2.modelID}` : "(default)"}`);
236
244
  recordContinue(sid);
237
245
  w.lastRetryAt = Date.now();
238
246
  } catch (err) {
@@ -241,7 +249,7 @@ var AutoResumePlugin = async (ctx, options) => {
241
249
  try {
242
250
  await ctx.client.session.prompt({
243
251
  path: { id: sid },
244
- body: { parts: [{ type: "text", text }] }
252
+ body: { parts: [{ type: "text", text }], agent, model }
245
253
  });
246
254
  recordContinue(sid);
247
255
  w.lastRetryAt = Date.now();
@@ -263,32 +271,37 @@ var AutoResumePlugin = async (ctx, options) => {
263
271
  return response.messages;
264
272
  return [];
265
273
  }
266
- async function checkSubagentCrashed(parentSid) {
274
+ async function checkSubagentStatus(parentSid) {
267
275
  try {
268
276
  const response = await ctx.client.session.list();
269
- const sessions2 = extractMessages(response);
270
- for (const s of sessions2) {
277
+ const allSessions = extractMessages(response);
278
+ let hasBusySubagent = false;
279
+ for (const s of allSessions) {
271
280
  const sId = s.id;
272
281
  if (!sId || sId === parentSid)
273
282
  continue;
274
283
  const status = s.status;
275
284
  if (status === "busy") {
285
+ hasBusySubagent = true;
276
286
  const msgResponse = await ctx.client.session.messages({ path: { id: sId } });
277
287
  const messages = extractMessages(msgResponse);
278
288
  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;
289
+ const rawRole = lastMsg?.role ?? lastMsg?.info?.role;
290
+ if (lastMsg && rawRole === "assistant" && (("error" in lastMsg) || lastMsg.info && ("error" in lastMsg.info))) {
291
+ await log("debug", `Subagent ${short(sId)} appears crashed`);
292
+ return "crashed";
284
293
  }
285
294
  }
286
295
  }
296
+ if (!hasBusySubagent) {
297
+ return "idle";
298
+ }
299
+ return "busy";
287
300
  } catch (err) {
288
301
  const errMsg = err instanceof Error ? err.message : String(err);
289
- await log("debug", `checkSubagentCrashed failed: ${errMsg}`);
302
+ await log("debug", `checkSubagentStatus failed: ${errMsg}`);
303
+ return "unknown";
290
304
  }
291
- return false;
292
305
  }
293
306
  function resetSessionFlags(w) {
294
307
  w.userCancelled = false;
@@ -328,9 +341,10 @@ var AutoResumePlugin = async (ctx, options) => {
328
341
  });
329
342
  const messages = extractMessages(response);
330
343
  const recent = messages.slice(-3);
344
+ let bestCandidate = null;
331
345
  for (const msg of recent) {
332
- const role = msg.role;
333
- if (role !== "assistant")
346
+ const rawRole = msg.role ?? msg.info?.role;
347
+ if (rawRole !== "assistant")
334
348
  continue;
335
349
  const parts = msg.parts;
336
350
  if (!parts)
@@ -348,45 +362,44 @@ var AutoResumePlugin = async (ctx, options) => {
348
362
  continue;
349
363
  }
350
364
  if (containsToolCallAsText(text)) {
351
- w.toolTextRecovered = true;
352
- w.toolTextAttempts++;
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...`);
356
- if (isHallucinationLoop(sid)) {
357
- await log("warn", `Hallucination loop detected on ${short(sid)} \u2014 aborting instead`);
358
- await tryAbortAndResume(sid, w);
359
- } else {
360
- try {
361
- await sendContinuePrompt(sid, prompt, w);
362
- await log("info", `${short(sid)} - tool-call-as-text recovery sent (attempt ${w.toolTextAttempts})`);
363
- } catch (err) {
364
- const errMsg = err instanceof Error ? err.message : String(err);
365
- await log("warn", `${short(sid)} - tool-call-as-text recovery failed: ${errMsg}`);
366
- }
365
+ const candidate = {
366
+ prompt: isReasoning ? THINKING_TOOL_RECOVERY_PROMPT : TOOL_TEXT_RECOVERY_PROMPT,
367
+ source: isReasoning ? "reasoning" : "text",
368
+ priority: 0
369
+ };
370
+ if (!bestCandidate || candidate.priority < bestCandidate.priority) {
371
+ bestCandidate = candidate;
367
372
  }
368
- return;
369
373
  }
370
374
  if (containsReadyToContinuePattern(text)) {
371
- w.toolTextRecovered = true;
372
- w.toolTextAttempts++;
373
- await log("info", `Ready-to-continue pattern detected on ${short(sid)}! ` + `Attempt ${w.toolTextAttempts}/${maxRetries}. Sending continue...`);
374
- if (isHallucinationLoop(sid)) {
375
- await log("warn", `Hallucination loop detected on ${short(sid)} \u2014 aborting instead`);
376
- await tryAbortAndResume(sid, w);
377
- } else {
378
- try {
379
- await sendContinuePrompt(sid, "continue", w);
380
- await log("info", `${short(sid)} - ready-to-continue recovery sent (attempt ${w.toolTextAttempts})`);
381
- } catch (err) {
382
- const errMsg = err instanceof Error ? err.message : String(err);
383
- await log("warn", `${short(sid)} - ready-to-continue recovery failed: ${errMsg}`);
384
- }
375
+ const candidate = {
376
+ prompt: "continue",
377
+ source: "ready-to-continue",
378
+ priority: 1
379
+ };
380
+ if (!bestCandidate || candidate.priority < bestCandidate.priority) {
381
+ bestCandidate = candidate;
385
382
  }
386
- return;
387
383
  }
388
384
  }
389
385
  }
386
+ if (!bestCandidate)
387
+ return;
388
+ w.toolTextRecovered = true;
389
+ w.toolTextAttempts++;
390
+ await log("info", `${bestCandidate.source} detected on ${short(sid)}! ` + `Attempt ${w.toolTextAttempts}/${maxRetries}. Sending recovery prompt...`);
391
+ if (isHallucinationLoop(sid)) {
392
+ await log("warn", `Hallucination loop detected on ${short(sid)} \u2014 aborting instead`);
393
+ await tryAbortAndResume(sid, w);
394
+ } else {
395
+ try {
396
+ await sendContinuePrompt(sid, bestCandidate.prompt, w);
397
+ await log("info", `${short(sid)} - ${bestCandidate.source} recovery sent (attempt ${w.toolTextAttempts})`);
398
+ } catch (err) {
399
+ const errMsg = err instanceof Error ? err.message : String(err);
400
+ await log("warn", `${short(sid)} - ${bestCandidate.source} recovery failed: ${errMsg}`);
401
+ }
402
+ }
390
403
  } catch (err) {
391
404
  const errMsg = err instanceof Error ? err.message : String(err);
392
405
  log("debug", `${short(sid)} - could not fetch messages: ${errMsg}`);
@@ -497,10 +510,13 @@ var AutoResumePlugin = async (ctx, options) => {
497
510
  const orphanIdle = now - w.orphanWatchStartAt;
498
511
  if (orphanIdle >= subagentWaitMs + gracePeriodMs) {
499
512
  if (w.resumeAttempts < maxRetries) {
500
- const crashed = await checkSubagentCrashed(sid);
501
- if (crashed) {
513
+ const subStatus = await checkSubagentStatus(sid);
514
+ if (subStatus === "crashed") {
502
515
  await log("info", `Subagent crashed, triggering abort+resume on ${short(sid)}`);
503
516
  tryAbortAndResume(sid, w);
517
+ } else if (subStatus === "idle") {
518
+ await log("info", `All subagents idle, parent ${short(sid)} stuck. Triggering abort+resume.`);
519
+ tryAbortAndResume(sid, w);
504
520
  } else {
505
521
  await log("debug", `Subagent still running, waiting...`);
506
522
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-auto-resume",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "OpenCode plugin that automatically resumes stalled LLM sessions when thinking/streaming freezes mid-generation.",
5
5
  "keywords": [
6
6
  "opencode",