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.
Files changed (2) hide show
  1. package/dist/index.js +66 -101
  2. 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 data = response;
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 ctx.client.session.prompt({
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 ctx.client.session.prompt({
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 ctx.client.session.prompt({
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 ctx.client.session.prompt({
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 data = response;
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
- const agent = await getSessionAgent(sid);
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.userCancelled = false;
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.userCancelled = false;
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.userCancelled = false;
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.orphanWatchStartAt = null;
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.userCancelled = false;
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",
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
- "scripts": {
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"