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 +2 -0
- package/dist/index.js +84 -7
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
349
|
+
}
|
|
281
350
|
if (containsToolCallAsText(text)) {
|
|
282
351
|
w.toolTextRecovered = true;
|
|
283
352
|
w.toolTextAttempts++;
|
|
284
|
-
|
|
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,
|
|
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
|
-
|
|
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