opencode-auto-resume 1.0.3 → 1.0.5
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/dist/index.js +66 -101
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -119,7 +119,8 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
119
119
|
toolTextRecovered: false,
|
|
120
120
|
toolTextAttempts: 0,
|
|
121
121
|
continueTimestamps: [],
|
|
122
|
-
idleSince: null
|
|
122
|
+
idleSince: null,
|
|
123
|
+
continuing: false
|
|
123
124
|
};
|
|
124
125
|
sessions.set(sid, w);
|
|
125
126
|
}
|
|
@@ -202,7 +203,53 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
202
203
|
log("debug", `Cleaned up ${toDelete.length} idle session(s). Map size: ${sessions.size}`);
|
|
203
204
|
}
|
|
204
205
|
}
|
|
206
|
+
async function sendContinuePrompt(sid, text, w) {
|
|
207
|
+
if (w.continuing) {
|
|
208
|
+
await log("debug", `${short(sid)} - continue already in progress, skipping`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
w.continuing = true;
|
|
212
|
+
try {
|
|
213
|
+
await ctx.client.session.prompt({
|
|
214
|
+
path: { id: sid },
|
|
215
|
+
body: { parts: [{ type: "text", text }] }
|
|
216
|
+
});
|
|
217
|
+
recordContinue(sid);
|
|
218
|
+
w.lastRetryAt = Date.now();
|
|
219
|
+
} finally {
|
|
220
|
+
w.continuing = false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function extractMessages(response) {
|
|
224
|
+
if (Array.isArray(response))
|
|
225
|
+
return response;
|
|
226
|
+
if (Array.isArray(response.data))
|
|
227
|
+
return response.data;
|
|
228
|
+
if (Array.isArray(response.messages))
|
|
229
|
+
return response.messages;
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
function resetSessionFlags(w) {
|
|
233
|
+
w.userCancelled = false;
|
|
234
|
+
w.resumeAttempts = 0;
|
|
235
|
+
w.gaveUp = false;
|
|
236
|
+
w.orphanWatchStartAt = null;
|
|
237
|
+
w.aborting = false;
|
|
238
|
+
w.toolTextRecovered = false;
|
|
239
|
+
w.toolTextAttempts = 0;
|
|
240
|
+
w.continueTimestamps = [];
|
|
241
|
+
w.idleSince = null;
|
|
242
|
+
w.continuing = false;
|
|
243
|
+
}
|
|
244
|
+
function resetIdleFlags(w) {
|
|
245
|
+
w.userCancelled = false;
|
|
246
|
+
w.aborting = false;
|
|
247
|
+
w.orphanWatchStartAt = null;
|
|
248
|
+
w.idleSince = Date.now();
|
|
249
|
+
}
|
|
205
250
|
async function checkForToolCallAsText(sid, w) {
|
|
251
|
+
if (typeof sid !== "string" || !sid)
|
|
252
|
+
return;
|
|
206
253
|
if (w.userCancelled || w.toolTextRecovered)
|
|
207
254
|
return;
|
|
208
255
|
if (w.toolTextAttempts > 0) {
|
|
@@ -218,15 +265,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
218
265
|
const response = await ctx.client.session.messages({
|
|
219
266
|
path: { id: sid }
|
|
220
267
|
});
|
|
221
|
-
const
|
|
222
|
-
let messages = [];
|
|
223
|
-
if (Array.isArray(data)) {
|
|
224
|
-
messages = data;
|
|
225
|
-
} else if (Array.isArray(data.data)) {
|
|
226
|
-
messages = data.data;
|
|
227
|
-
} else if (Array.isArray(data.messages)) {
|
|
228
|
-
messages = data.messages;
|
|
229
|
-
}
|
|
268
|
+
const messages = extractMessages(response);
|
|
230
269
|
const recent = messages.slice(-3);
|
|
231
270
|
for (const msg of recent) {
|
|
232
271
|
const role = msg.role;
|
|
@@ -248,12 +287,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
248
287
|
await tryAbortAndResume(sid, w);
|
|
249
288
|
} else {
|
|
250
289
|
try {
|
|
251
|
-
await
|
|
252
|
-
path: { id: sid },
|
|
253
|
-
body: { agent: w.agent, parts: [{ type: "text", text: TOOL_TEXT_RECOVERY_PROMPT }] }
|
|
254
|
-
});
|
|
255
|
-
recordContinue(sid);
|
|
256
|
-
w.lastRetryAt = Date.now();
|
|
290
|
+
await sendContinuePrompt(sid, TOOL_TEXT_RECOVERY_PROMPT, w);
|
|
257
291
|
await log("info", `${short(sid)} - tool-call-as-text recovery sent (attempt ${w.toolTextAttempts})`);
|
|
258
292
|
} catch (err) {
|
|
259
293
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -271,12 +305,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
271
305
|
await tryAbortAndResume(sid, w);
|
|
272
306
|
} else {
|
|
273
307
|
try {
|
|
274
|
-
await
|
|
275
|
-
path: { id: sid },
|
|
276
|
-
body: { agent: w.agent, parts: [{ type: "text", text: "continue" }] }
|
|
277
|
-
});
|
|
278
|
-
recordContinue(sid);
|
|
279
|
-
w.lastRetryAt = Date.now();
|
|
308
|
+
await sendContinuePrompt(sid, "continue", w);
|
|
280
309
|
await log("info", `${short(sid)} - ready-to-continue recovery sent (attempt ${w.toolTextAttempts})`);
|
|
281
310
|
} catch (err) {
|
|
282
311
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -293,6 +322,8 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
293
322
|
}
|
|
294
323
|
}
|
|
295
324
|
async function tryAbortAndResume(sid, w) {
|
|
325
|
+
if (typeof sid !== "string" || !sid)
|
|
326
|
+
return false;
|
|
296
327
|
if (w.aborting)
|
|
297
328
|
return false;
|
|
298
329
|
w.aborting = true;
|
|
@@ -311,13 +342,8 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
311
342
|
if (w.status === "busy")
|
|
312
343
|
w.status = "idle";
|
|
313
344
|
try {
|
|
314
|
-
await
|
|
315
|
-
path: { id: sid },
|
|
316
|
-
body: { agent: w.agent, parts: [{ type: "text", text: "continue" }] }
|
|
317
|
-
});
|
|
318
|
-
recordContinue(sid);
|
|
345
|
+
await sendContinuePrompt(sid, "continue", w);
|
|
319
346
|
await log("info", `${short(sid)} - abort+continue done`);
|
|
320
|
-
w.lastRetryAt = Date.now();
|
|
321
347
|
w.orphanWatchStartAt = null;
|
|
322
348
|
w.resumeAttempts++;
|
|
323
349
|
w.aborting = false;
|
|
@@ -330,6 +356,10 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
330
356
|
}
|
|
331
357
|
}
|
|
332
358
|
async function tryResume(sid, w, reason) {
|
|
359
|
+
if (typeof sid !== "string" || !sid) {
|
|
360
|
+
await log("warn", `tryResume called with invalid sid: ${sid}`);
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
333
363
|
const now = Date.now();
|
|
334
364
|
const elapsedSinceRetry = now - w.lastRetryAt;
|
|
335
365
|
const requiredBackoff = backoffMs(w.resumeAttempts);
|
|
@@ -343,13 +373,8 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
343
373
|
const idleSec = Math.round((now - w.lastActivityAt) / 1000);
|
|
344
374
|
await log("info", `${reason} on ${short(sid)} (${idleSec}s, retry ${w.resumeAttempts}/${maxRetries})`);
|
|
345
375
|
try {
|
|
346
|
-
await
|
|
347
|
-
path: { id: sid },
|
|
348
|
-
body: { agent: w.agent, model: true, parts: [{ type: "text", text: "continue" }] }
|
|
349
|
-
});
|
|
350
|
-
recordContinue(sid);
|
|
376
|
+
await sendContinuePrompt(sid, "continue", w);
|
|
351
377
|
await log("info", `${short(sid)} - retry sent`);
|
|
352
|
-
w.lastRetryAt = now;
|
|
353
378
|
return true;
|
|
354
379
|
} catch (err) {
|
|
355
380
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -358,42 +383,10 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
358
383
|
return false;
|
|
359
384
|
}
|
|
360
385
|
}
|
|
361
|
-
async function getSessionAgent(sid) {
|
|
362
|
-
try {
|
|
363
|
-
const response = await ctx.client.session.messages({
|
|
364
|
-
path: { id: sid }
|
|
365
|
-
});
|
|
366
|
-
const data = response;
|
|
367
|
-
let messages = [];
|
|
368
|
-
if (Array.isArray(data)) {
|
|
369
|
-
messages = data;
|
|
370
|
-
} else if (Array.isArray(data.data)) {
|
|
371
|
-
messages = data.data;
|
|
372
|
-
} else if (Array.isArray(data.messages)) {
|
|
373
|
-
messages = data.messages;
|
|
374
|
-
}
|
|
375
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
376
|
-
const msg = messages[i];
|
|
377
|
-
const role = msg.role;
|
|
378
|
-
if (role === "assistant") {
|
|
379
|
-
const agent = msg.agent;
|
|
380
|
-
if (agent)
|
|
381
|
-
return agent;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
} catch {}
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
386
|
async function discoverSessions() {
|
|
388
387
|
try {
|
|
389
388
|
const response = await ctx.client.session.list();
|
|
390
|
-
const
|
|
391
|
-
let list = [];
|
|
392
|
-
if (Array.isArray(data)) {
|
|
393
|
-
list = data;
|
|
394
|
-
} else if (Array.isArray(data.data)) {
|
|
395
|
-
list = data.data;
|
|
396
|
-
}
|
|
389
|
+
const list = extractMessages(response);
|
|
397
390
|
for (const s of list) {
|
|
398
391
|
const sid = s.id;
|
|
399
392
|
if (sid) {
|
|
@@ -407,14 +400,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
407
400
|
w.idleSince = Date.now();
|
|
408
401
|
}
|
|
409
402
|
if (isNew) {
|
|
410
|
-
|
|
411
|
-
if (agent) {
|
|
412
|
-
const w = sessions.get(sid);
|
|
413
|
-
w.agent = agent;
|
|
414
|
-
log("debug", `Discovered session ${short(sid)} with agent: ${agent}`);
|
|
415
|
-
} else {
|
|
416
|
-
log("debug", `Discovered session ${short(sid)} via list()`);
|
|
417
|
-
}
|
|
403
|
+
log("debug", `Discovered session ${short(sid)} via list()`);
|
|
418
404
|
}
|
|
419
405
|
}
|
|
420
406
|
}
|
|
@@ -490,21 +476,11 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
490
476
|
w.status = statusType;
|
|
491
477
|
if (statusType === "busy") {
|
|
492
478
|
w.lastActivityAt = Date.now();
|
|
493
|
-
w
|
|
494
|
-
w.resumeAttempts = 0;
|
|
495
|
-
w.gaveUp = false;
|
|
496
|
-
w.orphanWatchStartAt = null;
|
|
497
|
-
w.aborting = false;
|
|
498
|
-
w.toolTextRecovered = false;
|
|
499
|
-
w.toolTextAttempts = 0;
|
|
500
|
-
w.continueTimestamps = [];
|
|
501
|
-
w.idleSince = null;
|
|
479
|
+
resetSessionFlags(w);
|
|
502
480
|
log("debug", `${short(sid)} -> busy (${busyCount()})`);
|
|
503
481
|
} else if (statusType === "idle") {
|
|
504
482
|
w.status = "idle";
|
|
505
|
-
w
|
|
506
|
-
w.aborting = false;
|
|
507
|
-
w.idleSince = Date.now();
|
|
483
|
+
resetIdleFlags(w);
|
|
508
484
|
const currentBusy = busyCount();
|
|
509
485
|
if (prevBusyCount > 1 && currentBusy === 1) {
|
|
510
486
|
const lone = getLoneBusySession();
|
|
@@ -544,10 +520,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
544
520
|
const w = sessions.get(sid);
|
|
545
521
|
if (w) {
|
|
546
522
|
w.status = "idle";
|
|
547
|
-
w
|
|
548
|
-
w.orphanWatchStartAt = null;
|
|
549
|
-
w.aborting = false;
|
|
550
|
-
w.idleSince = Date.now();
|
|
523
|
+
resetIdleFlags(w);
|
|
551
524
|
if (!w.toolTextRecovered && w.toolTextAttempts < maxRetries) {
|
|
552
525
|
setTimeout(() => {
|
|
553
526
|
checkForToolCallAsText(sid, w);
|
|
@@ -565,9 +538,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
565
538
|
if (w.status === "busy") {
|
|
566
539
|
w.userCancelled = true;
|
|
567
540
|
w.status = "idle";
|
|
568
|
-
w
|
|
569
|
-
w.aborting = false;
|
|
570
|
-
w.idleSince = Date.now();
|
|
541
|
+
resetIdleFlags(w);
|
|
571
542
|
}
|
|
572
543
|
}
|
|
573
544
|
log("info", "User abort (ESC)");
|
|
@@ -581,13 +552,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
581
552
|
}
|
|
582
553
|
case "command.executed": {
|
|
583
554
|
for (const [, w] of sessions) {
|
|
584
|
-
w
|
|
585
|
-
w.resumeAttempts = 0;
|
|
586
|
-
w.gaveUp = false;
|
|
587
|
-
w.orphanWatchStartAt = null;
|
|
588
|
-
w.aborting = false;
|
|
589
|
-
w.toolTextRecovered = false;
|
|
590
|
-
w.toolTextAttempts = 0;
|
|
555
|
+
resetSessionFlags(w);
|
|
591
556
|
}
|
|
592
557
|
break;
|
|
593
558
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-auto-resume",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "OpenCode plugin that automatically resumes stalled LLM sessions when thinking/streaming freezes mid-generation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
"author": "Daniele \"mte90\" Scasciafratte",
|
|
22
22
|
"type": "module",
|
|
23
23
|
"main": "dist/index.js",
|
|
24
|
-
|
|
24
|
+
"scripts": {
|
|
25
25
|
"build": "bun build src/index.ts --outdir dist --target bun",
|
|
26
26
|
"dev": "bun build src/index.ts --outdir dist --target bun --watch",
|
|
27
|
+
"test": "bun test",
|
|
27
28
|
"prepublishOnly": "bun run build"
|
|
28
|
-
|
|
29
|
-
"files": [
|
|
29
|
+
}, "files": [
|
|
30
30
|
"dist/index.js",
|
|
31
31
|
"README.md",
|
|
32
32
|
"LICENSE"
|