opencode-auto-resume 1.0.7 → 1.0.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.
- package/README.md +6 -0
- package/dist/index.js +60 -11
- 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
|
|
221
|
-
if (
|
|
222
|
-
const rawAgent =
|
|
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
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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) {
|
package/package.json
CHANGED