opencode-auto-resume 1.1.18 → 1.1.20

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 (3) hide show
  1. package/README.md +10 -7
  2. package/dist/index.js +126 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -10,7 +10,7 @@ LLM sessions fail in predictable ways. This plugin monitors all sessions and aut
10
10
 
11
11
  ### Stall recovery
12
12
 
13
- 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 (`chunkTimeoutMs` + `gracePeriodMs`), the plugin sends `"continue"` with exponential backoff. After 3 failed attempts it gives up.
13
+ The stream goes silent but the session stays "busy". The UI shows a blinking cursor with no progress. If no events arrive for ~3 minutes (`chunkTimeoutMs` + `gracePeriodMs`), the plugin sends `"continue"` with exponential backoff. After 3 failed attempts it gives up.
14
14
 
15
15
  The `busyStallStrategy` option controls this path: `"continue"` (default), `"abort"` (abort-first), or `"off"` — see [Recovery model](#recovery-model) for what a busy prompt can and cannot do while the runner is live.
16
16
 
@@ -250,7 +250,7 @@ If the assistant claims the task is done ("task done", "finished", "all complete
250
250
  The session is not stalled while the ball is in the user's court. Two gates stand down idle nudges:
251
251
 
252
252
  - **Awaiting input**: the newest assistant message holds a `tool_use` part with `state.status: "pending"` (e.g. an open `question` tool call). All idle checks, the periodic recheck, delayed action-intent callbacks, and the tool-text scan skip prompting until a newer user message clears the gate. Completed tool calls never engage it.
253
- - **Recently active user**: any inbound user message within `activeUserWindowMs` (default 15 minutes) means the user is engaged — likely composing a reply, which leaves no pending tool call behind. Open-todos nudges (idle + periodic), the tool-text reminder fallback, and action-intent callbacks stand down until the window expires.
253
+ - **Recently active user**: any inbound user message within `activeUserWindowMs` (default 5 minutes) means the user is engaged — likely composing a reply, which leaves no pending tool call behind. Open-todos nudges (idle + periodic), the tool-text reminder fallback, and action-intent callbacks stand down until the window expires.
254
254
 
255
255
  ---
256
256
 
@@ -367,7 +367,7 @@ With options:
367
367
  {
368
368
  "plugin": [
369
369
  ["opencode-auto-resume", {
370
- "chunkTimeoutMs": 45000,
370
+ "chunkTimeoutMs": 180000,
371
371
  "gracePeriodMs": 3000,
372
372
  "maxRetries": 3
373
373
  }]
@@ -382,7 +382,7 @@ With options:
382
382
  "plugin": [
383
383
  [
384
384
  "file:///home/YOURUSER/.config/opencode/plugins/opencode-auto-resume/dist/index.js",
385
- { "chunkTimeoutMs": 45000, "maxRetries": 3 }
385
+ { "chunkTimeoutMs": 180000, "maxRetries": 3 }
386
386
  ]
387
387
  ]
388
388
  }
@@ -392,7 +392,7 @@ With options:
392
392
 
393
393
  | Option | Default | Description |
394
394
  |---|---|---|
395
- | `chunkTimeoutMs` | `45000` | Inactivity timeout before considering stream stalled |
395
+ | `chunkTimeoutMs` | `180000` | Inactivity timeout before considering stream stalled |
396
396
  | `gracePeriodMs` | `3000` | Extra wait before acting (lets ESC/status events arrive) |
397
397
  | `checkIntervalMs` | `5000` | Timer poll interval |
398
398
  | `maxRetries` | `3` | Max auto-resume attempts before giving up |
@@ -407,6 +407,7 @@ With options:
407
407
  | `toolTextCheckDelayMs` | `3000` | Delay before scanning an idle session for tool-as-text; also the recovery watchdog delay |
408
408
  | `minActivityGapMs` | `1000` | Skip recovery if the session was active within this gap |
409
409
  | `warmupMs` | `15000` | Action-intent detection disabled while a session is younger than this |
410
+ | `discoveryDelayMs` | `5000` | Delay before the initial session discovery run after plugin attach |
410
411
  | `debug` | `false` | Enable `[debug]` console diagnostics |
411
412
  | `resumeOnActionIntent` | `true` | Enable action-intent (`:`-terminated line) nudges |
412
413
  | `continuePrompt` | `"continue"` | Prompt text for stall/streaming/dead-stream recovery |
@@ -415,10 +416,12 @@ With options:
415
416
  | `thinkingToolRecoveryPrompt` | `THINKING_TOOL_RECOVERY_PROMPT` | Override the thinking-tool recovery prompt |
416
417
  | `doneWithoutWorkPrompt` | `DONE_WITHOUT_WORK_PROMPT` | Override the done-claim-with-open-todos prompt |
417
418
  | `doneWithoutDetailsPrompt` | `DONE_WITHOUT_DETAILS_PROMPT` | Override the done-claim-with-no-todos report prompt |
419
+ | `doneClaimPatterns` | `DONE_CLAIM_PATTERNS` | Array of regex strings overriding the default done-claim detection patterns (case-insensitive, multiline). Invalid regexes are skipped. Empty array falls back to defaults. |
420
+ | `readyToContinuePatterns` | `READY_TO_CONTINUE_PATTERNS` | Array of regex strings overriding the default ready-to-continue detection patterns (case-insensitive). Invalid regexes are skipped. Empty array falls back to defaults. |
418
421
  | `silentDeadStreamMinTokens` | `200` | Min output tokens to treat a textless `finish:"unknown"` message as a dead stream |
419
422
  | `busyStallStrategy` | `"continue"` | Busy-stall response: `"continue"`, `"abort"` (abort-first), or `"off"` (disabled) |
420
423
  | `contextSaturationThreshold` | `0.85` | Ratio of used/usable context that routes a saturated parent to magic-context `ctx-wrapup` (only when magic-context is installed) |
421
- | `activeUserWindowMs` | `900000` | Inbound-user-message recency window (15 min) during which idle nudges stand down (user likely composing) |
424
+ | `activeUserWindowMs` | `300000` | Inbound-user-message recency window (5 min) during which idle nudges stand down (user likely composing) |
422
425
  | `subagentNativeCompactionEnabled` | `false` | Opt-in native `session.summarize()` for saturated subagent sessions (no magic-context detection required) |
423
426
 
424
427
  Message patterns are matched case-insensitively. Error names use exact match.
@@ -443,7 +446,7 @@ The plugin handles all recovery automatically — no manual intervention needed.
443
446
  | Problem | Solution |
444
447
  |---|---|
445
448
  | Resumes after ESC | Increase `gracePeriodMs` to `5000` |
446
- | Too aggressive | Increase `chunkTimeoutMs` to `60000` |
449
+ | Stall recovery too aggressive | Decrease `chunkTimeoutMs` (e.g. `45000`) |
447
450
  | Too slow to react | Decrease `checkIntervalMs` to `2000` |
448
451
  | Orphan parent not detected | Increase `subagentWaitMs` to `20000` |
449
452
  | Hallucination loop not caught | Decrease `loopMaxContinues` to `2` |
package/dist/index.js CHANGED
@@ -12335,9 +12335,10 @@ function tool(input) {
12335
12335
  }
12336
12336
  tool.schema = exports_external;
12337
12337
  // src/index.ts
12338
- var DEFAULT_CHUNK_TIMEOUT_MS = 45000;
12338
+ var DEFAULT_CHUNK_TIMEOUT_MS = 180000;
12339
12339
  var DEFAULT_CHECK_INTERVAL_MS = 5000;
12340
- var DEFAULT_ACTIVE_USER_WINDOW_MS = 15 * 60000;
12340
+ var DEFAULT_DISCOVERY_DELAY_MS = 5000;
12341
+ var DEFAULT_ACTIVE_USER_WINDOW_MS = 5 * 60000;
12341
12342
  var DEFAULT_GRACE_PERIOD_MS = 3000;
12342
12343
  var DEFAULT_MAX_RETRIES = 3;
12343
12344
  var DEFAULT_MAX_BACKOFF_MS = 8000;
@@ -12352,6 +12353,7 @@ var DEFAULT_MIN_ACTIVITY_GAP_MS = 1000;
12352
12353
  var DEFAULT_WARMUP_MS = 15000;
12353
12354
  var DEFAULT_SILENT_DEAD_STREAM_MIN_TOKENS = 200;
12354
12355
  var DEFAULT_DEBUG = false;
12356
+ var OOC_ERROR_RE = /exceeds the available context size|context size \(\d+\)|too large to compact|too many tokens|prompt is too long/i;
12355
12357
  var DEFAULT_STREAMING_FAILURE_ERROR_NAMES = [
12356
12358
  "ProviderError",
12357
12359
  "APIError",
@@ -12428,7 +12430,7 @@ function containsToolCallAsText(text) {
12428
12430
  }
12429
12431
  return false;
12430
12432
  }
12431
- function containsReadyToContinuePattern(text) {
12433
+ function containsReadyToContinuePattern(text, patterns = READY_TO_CONTINUE_PATTERNS) {
12432
12434
  const lines = text.split(`
12433
12435
  `);
12434
12436
  const lastLine = lines[lines.length - 1]?.trim();
@@ -12436,7 +12438,7 @@ function containsReadyToContinuePattern(text) {
12436
12438
  return false;
12437
12439
  const lastLines = lines.slice(-3).join(`
12438
12440
  `);
12439
- return READY_TO_CONTINUE_PATTERNS.some((pat) => pat.test(lastLines));
12441
+ return patterns.some((pat) => pat.test(lastLines));
12440
12442
  }
12441
12443
  var DONE_CLAIM_PATTERNS = [
12442
12444
  /^task\s+done[.!]*$/im,
@@ -12456,12 +12458,12 @@ var DONE_CLAIM_PATTERNS = [
12456
12458
  ];
12457
12459
  var DONE_WITHOUT_WORK_PROMPT = "I need you to verify more carefully that you have actually completed all the required tasks. " + "Your response indicated you're done, but no work was detected. Please check your todo list " + "and complete any remaining work.";
12458
12460
  var DONE_WITHOUT_DETAILS_PROMPT = "Your last response claimed the task is complete but contained no work description. This is not acceptable. " + "You MUST respond now with a full, detailed report of everything you did: " + "for each file you modified, state the full path and the exact changes; " + "list every command you ran to verify and its result; state the final outcome. " + "Do NOT reply with 'done', 'task completed', or any short acknowledgment \u2014 " + "your ONLY acceptable response right now is this detailed report. Write it now.";
12459
- function containsDoneClaimPattern(text) {
12461
+ function containsDoneClaimPattern(text, patterns = DONE_CLAIM_PATTERNS) {
12460
12462
  const lines = text.split(`
12461
12463
  `);
12462
12464
  const lastLines = lines.slice(-5).join(`
12463
12465
  `);
12464
- return DONE_CLAIM_PATTERNS.some((pat) => pat.test(lastLines));
12466
+ return patterns.some((pat) => pat.test(lastLines));
12465
12467
  }
12466
12468
  function containsWorkDescription(text) {
12467
12469
  if (/`[^`\n]*\.[a-zA-Z0-9]{1,8}[^`\n]*`/.test(text))
@@ -12659,6 +12661,7 @@ var AutoResumePlugin = async (ctx, options) => {
12659
12661
  const maxRecoveryRetries = options?.maxRecoveryRetries ?? DEFAULT_MAX_RECOVERY_RETRIES;
12660
12662
  const minActivityGapMs = options?.minActivityGapMs ?? DEFAULT_MIN_ACTIVITY_GAP_MS;
12661
12663
  const warmupMs = options?.warmupMs ?? DEFAULT_WARMUP_MS;
12664
+ const discoveryDelayMs = options?.discoveryDelayMs ?? DEFAULT_DISCOVERY_DELAY_MS;
12662
12665
  const debug = options?.debug ?? DEFAULT_DEBUG;
12663
12666
  const streamingFailureErrorNames = options?.streamingFailureErrorNames ?? DEFAULT_STREAMING_FAILURE_ERROR_NAMES;
12664
12667
  const streamingFailureMessagePatterns = options?.streamingFailureMessagePatterns ?? DEFAULT_STREAMING_FAILURE_MESSAGE_PATTERNS;
@@ -12675,6 +12678,30 @@ var AutoResumePlugin = async (ctx, options) => {
12675
12678
  const contextSaturationThreshold = options?.contextSaturationThreshold ?? 0.85;
12676
12679
  const subagentNativeCompactionEnabled = options?.subagentNativeCompactionEnabled ?? false;
12677
12680
  const activeUserWindowMs = options?.activeUserWindowMs ?? DEFAULT_ACTIVE_USER_WINDOW_MS;
12681
+ const doneClaimPatterns = (() => {
12682
+ const raw = options?.doneClaimPatterns;
12683
+ if (!Array.isArray(raw) || raw.length === 0)
12684
+ return DONE_CLAIM_PATTERNS;
12685
+ return raw.map((s) => {
12686
+ try {
12687
+ return new RegExp(s, "im");
12688
+ } catch {
12689
+ return null;
12690
+ }
12691
+ }).filter((r) => r !== null);
12692
+ })();
12693
+ const readyToContinuePatterns = (() => {
12694
+ const raw = options?.readyToContinuePatterns;
12695
+ if (!Array.isArray(raw) || raw.length === 0)
12696
+ return READY_TO_CONTINUE_PATTERNS;
12697
+ return raw.map((s) => {
12698
+ try {
12699
+ return new RegExp(s, "i");
12700
+ } catch {
12701
+ return null;
12702
+ }
12703
+ }).filter((r) => r !== null);
12704
+ })();
12678
12705
  const dbg = (...args) => {
12679
12706
  if (debug)
12680
12707
  console.log("[debug]", ...args);
@@ -12746,9 +12773,12 @@ var AutoResumePlugin = async (ctx, options) => {
12746
12773
  resumeAttempts: 0,
12747
12774
  lastRetryAt: 0,
12748
12775
  gaveUp: false,
12776
+ oocLocked: false,
12777
+ oocLockReason: null,
12749
12778
  orphanWatchStartAt: null,
12750
12779
  aborting: false,
12751
12780
  pluginAbortInFlight: false,
12781
+ pluginAbortAt: 0,
12752
12782
  toolTextRecovered: false,
12753
12783
  toolTextAttempts: 0,
12754
12784
  continueTimestamps: [],
@@ -12917,6 +12947,14 @@ var AutoResumePlugin = async (ctx, options) => {
12917
12947
  }
12918
12948
  if (w.userCancelled || w.completionSignaled)
12919
12949
  return;
12950
+ if (w.gaveUp) {
12951
+ await log("debug", `${short(sid)} - gaveUp latched, refusing further continue prompts`);
12952
+ return;
12953
+ }
12954
+ if (w.oocLocked) {
12955
+ await log("debug", `${short(sid)} - oocLocked latched, refusing further continue prompts (reason: ${w.oocLockReason ?? "n/a"})`);
12956
+ return;
12957
+ }
12920
12958
  if (!w.continuing)
12921
12959
  dbg(`State transition on ${short(sid)}: continuing=false -> true`);
12922
12960
  w.continuing = true;
@@ -13018,7 +13056,6 @@ var AutoResumePlugin = async (ctx, options) => {
13018
13056
  } catch (err) {
13019
13057
  const errMsg = err instanceof Error ? err.message : String(err);
13020
13058
  await log("warn", `${short(sid)} - recovery retry failed: ${errMsg}`);
13021
- w.recoveryAttempts = 0;
13022
13059
  }
13023
13060
  w.watchdogRetryGuard = false;
13024
13061
  } else {
@@ -13030,8 +13067,11 @@ var AutoResumePlugin = async (ctx, options) => {
13030
13067
  dbg(`Watchdog check on ${short(sid)}: status=${w.status}, recoveryAttempts=${w.recoveryAttempts}, maxRetries=${maxRecoveryRetries}, watchdogLatencyMs=${Date.now() - w.lastRetryAt} -> ABORT_RESUME`);
13031
13068
  const resumed = await tryAbortAndResume(sid, w);
13032
13069
  if (!resumed && !w.aborting) {
13033
- await log("warn", `Recovery exhausted on ${short(sid)}: attempts=${w.recoveryAttempts}, lastError=abort+resume failed`);
13034
- dbg(`Watchdog check on ${short(sid)}: status=${w.status} -> GAVE_UP`);
13070
+ w.gaveUp = true;
13071
+ w.pendingRecovery = false;
13072
+ w.pendingRecoveryReason = null;
13073
+ await log("warn", `Recovery exhausted on ${short(sid)}: attempts=${w.recoveryAttempts}, lastError=abort+resume failed \u2014 GAVE UP (re-arms on next user message)`);
13074
+ dbg(`Watchdog check on ${short(sid)}: status=${w.status} -> GAVE_UP (latched)`);
13035
13075
  if (w.pendingRecoveryAt > 0) {
13036
13076
  dbg(`Total recovery cycle on ${short(sid)} (failed): totalCycleMs=${Date.now() - w.pendingRecoveryAt}`);
13037
13077
  }
@@ -13660,7 +13700,7 @@ var AutoResumePlugin = async (ctx, options) => {
13660
13700
  bestCandidate = candidate;
13661
13701
  }
13662
13702
  }
13663
- if (containsReadyToContinuePattern(text)) {
13703
+ if (containsReadyToContinuePattern(text, readyToContinuePatterns)) {
13664
13704
  const todos = w.todos || [];
13665
13705
  const hasOpenTodos = todos.some(isOpenTodo);
13666
13706
  if (!hasOpenTodos && todos.length > 0) {
@@ -13680,16 +13720,42 @@ var AutoResumePlugin = async (ctx, options) => {
13680
13720
  await log("info", `${short(sid)} - skipping continue, todos appear completed (attempt ${w.todoCheckAttempts}/2)`);
13681
13721
  continue;
13682
13722
  }
13683
- const candidate = {
13684
- prompt: containsDoneClaimPattern(text) ? doneWithoutWorkPrompt : continuePrompt,
13685
- source: containsDoneClaimPattern(text) ? "done-claim" : "ready-to-continue",
13686
- priority: 1
13687
- };
13688
- if (!bestCandidate || candidate.priority < bestCandidate.priority) {
13689
- bestCandidate = candidate;
13723
+ if (containsDoneClaimPattern(text, doneClaimPatterns)) {
13724
+ const todos = w.todos || [];
13725
+ const hasOpenTodos = todos.some(isOpenTodo);
13726
+ if (hasOpenTodos) {
13727
+ const candidate = {
13728
+ prompt: doneWithoutWorkPrompt,
13729
+ source: "done-claim",
13730
+ priority: 1
13731
+ };
13732
+ if (!bestCandidate || candidate.priority < bestCandidate.priority) {
13733
+ bestCandidate = candidate;
13734
+ }
13735
+ } else if (!containsWorkDescription(allAssistantText)) {
13736
+ if (w.doneClaimNoTodosAttempts < maxRetries) {
13737
+ const candidate = {
13738
+ prompt: doneWithoutDetailsPrompt,
13739
+ source: "done-claim-no-todos",
13740
+ priority: 1
13741
+ };
13742
+ if (!bestCandidate || candidate.priority < bestCandidate.priority) {
13743
+ bestCandidate = candidate;
13744
+ }
13745
+ }
13746
+ }
13747
+ } else {
13748
+ const candidate = {
13749
+ prompt: continuePrompt,
13750
+ source: "ready-to-continue",
13751
+ priority: 1
13752
+ };
13753
+ if (!bestCandidate || candidate.priority < bestCandidate.priority) {
13754
+ bestCandidate = candidate;
13755
+ }
13690
13756
  }
13691
13757
  }
13692
- if (!bestCandidate && containsDoneClaimPattern(text)) {
13758
+ if (!bestCandidate && containsDoneClaimPattern(text, doneClaimPatterns)) {
13693
13759
  const todos = w.todos || [];
13694
13760
  const hasOpenTodos = todos.some(isOpenTodo);
13695
13761
  if (hasOpenTodos) {
@@ -13780,7 +13846,7 @@ var AutoResumePlugin = async (ctx, options) => {
13780
13846
  w.toolTextRecovered = true;
13781
13847
  w.toolTextAttempts++;
13782
13848
  }
13783
- const attemptNum = isOpenTodosReminder ? w.todoNudgeAttempts : isDoneClaimNoTodos ? w.doneClaimNoTodosAttempts : w.toolTextAttempts;
13849
+ const attemptNum = isOpenTodosReminder ? w.todoNudgeAttempts + 1 : isDoneClaimNoTodos ? w.doneClaimNoTodosAttempts : w.toolTextAttempts;
13784
13850
  await log("info", `${bestCandidate.source} detected on ${short(sid)}! ` + `Attempt ${attemptNum}/${maxRetries}. Sending recovery prompt...`);
13785
13851
  const timeSinceActivity = Date.now() - w.lastActivityAt;
13786
13852
  if (timeSinceActivity < minActivityGapMs) {
@@ -13826,9 +13892,12 @@ var AutoResumePlugin = async (ctx, options) => {
13826
13892
  return false;
13827
13893
  if (w.aborting)
13828
13894
  return false;
13895
+ if (w.oocLocked)
13896
+ return false;
13829
13897
  const idleSec = Math.round((Date.now() - (w.orphanWatchStartAt ?? w.lastActivityAt)) / 1000);
13830
13898
  await log("info", `Abort+Resume on ${short(sid)} (${idleSec}s idle). Aborting...`);
13831
13899
  w.pluginAbortInFlight = true;
13900
+ w.pluginAbortAt = Date.now();
13832
13901
  try {
13833
13902
  await ctx.client.session.abort({ path: { id: sid } });
13834
13903
  await log("info", `${short(sid)} - abort OK`);
@@ -13838,6 +13907,7 @@ var AutoResumePlugin = async (ctx, options) => {
13838
13907
  await log("warn", `${short(sid)} - abort failed: ${errMsg}`);
13839
13908
  w.aborting = false;
13840
13909
  w.pluginAbortInFlight = false;
13910
+ w.pluginAbortAt = 0;
13841
13911
  return false;
13842
13912
  }
13843
13913
  await new Promise((resolve) => setTimeout(resolve, ABORT_CONTINUE_DELAY_MS));
@@ -13862,6 +13932,7 @@ var AutoResumePlugin = async (ctx, options) => {
13862
13932
  return false;
13863
13933
  } finally {
13864
13934
  w.pluginAbortInFlight = false;
13935
+ w.pluginAbortAt = 0;
13865
13936
  }
13866
13937
  }
13867
13938
  async function tryResume(sid, w, reason, prompt) {
@@ -13907,12 +13978,13 @@ var AutoResumePlugin = async (ctx, options) => {
13907
13978
  try {
13908
13979
  const response = await ctx.client.session.list();
13909
13980
  const list = extractMessages(response);
13981
+ const statusMap = await getSessionStatusMap();
13910
13982
  for (const s of list) {
13911
13983
  const sid = s.id;
13912
13984
  if (sid && typeof sid === "string" && sid.startsWith("ses_")) {
13913
13985
  const isNew = !sessions.has(sid);
13914
13986
  ensureWatch(sid);
13915
- const status = s.status;
13987
+ const status = s.status ?? statusMap[sid];
13916
13988
  if (status) {
13917
13989
  const w = sessions.get(sid);
13918
13990
  w.status = status;
@@ -14155,7 +14227,7 @@ var AutoResumePlugin = async (ctx, options) => {
14155
14227
  }, SESSION_DISCOVERY_INTERVAL_MS);
14156
14228
  if (discoveryTimer.unref)
14157
14229
  discoveryTimer.unref();
14158
- setTimeout(discoverSessions, 5000);
14230
+ setTimeout(discoverSessions, discoveryDelayMs);
14159
14231
  }
14160
14232
  startTimer();
14161
14233
  async function handleEvent(ev) {
@@ -14176,7 +14248,11 @@ var AutoResumePlugin = async (ctx, options) => {
14176
14248
  if (w.pendingRecovery) {
14177
14249
  dbg(`Pending recovery cleared on ${short(sid)}: reason=session-busy`);
14178
14250
  }
14179
- resetBusyFlags(w);
14251
+ if (w.continuing) {
14252
+ dbg(`busy on ${short(sid)} via recovery prompt - preserving resumeAttempts=${w.resumeAttempts} gaveUp=${w.gaveUp}`);
14253
+ } else {
14254
+ resetBusyFlags(w);
14255
+ }
14180
14256
  prevBusyCount = busyCount();
14181
14257
  log("debug", `${short(sid)} -> busy (${prevBusyCount})`);
14182
14258
  } else if (statusType === "interrupted") {
@@ -14318,12 +14394,14 @@ var AutoResumePlugin = async (ctx, options) => {
14318
14394
  if (isCelebration) {
14319
14395
  await log("info", `${short(sid)} - \uD83C\uDF89 detected but ${open.length} open todos remain, sending nudge`);
14320
14396
  const reminder = buildOpenTodosReminder(todos);
14321
- await tryResume(sid, w, "Idle with open todos (celebration false positive)", reminder);
14322
- w.todoNudgeAttempts++;
14397
+ const sent = await tryResume(sid, w, "Idle with open todos (celebration false positive)", reminder);
14398
+ if (sent)
14399
+ w.todoNudgeAttempts++;
14323
14400
  } else {
14324
14401
  const reminder = buildOpenTodosReminder(todos);
14325
- await tryResume(sid, w, "Idle with open todos", reminder);
14326
- w.todoNudgeAttempts++;
14402
+ const sent = await tryResume(sid, w, "Idle with open todos", reminder);
14403
+ if (sent)
14404
+ w.todoNudgeAttempts++;
14327
14405
  }
14328
14406
  }
14329
14407
  }
@@ -14526,10 +14604,16 @@ var AutoResumePlugin = async (ctx, options) => {
14526
14604
  const isMessageAborted = errorName === "MessageAbortedError";
14527
14605
  if (isMessageAborted) {
14528
14606
  for (const [wSid, w] of sessions) {
14529
- if (!w.pluginAbortInFlight) {
14607
+ const PLUGIN_ABORT_GRACE_MS = 1000;
14608
+ const isOwnAbort = w.pluginAbortInFlight && Date.now() - (w.pluginAbortAt || 0) < PLUGIN_ABORT_GRACE_MS;
14609
+ if (!isOwnAbort) {
14530
14610
  w.userCancelled = true;
14531
14611
  w.status = "idle";
14532
14612
  resetIdleFlags(w);
14613
+ if (w.toolTextTimer) {
14614
+ clearTimeout(w.toolTextTimer);
14615
+ w.toolTextTimer = null;
14616
+ }
14533
14617
  }
14534
14618
  }
14535
14619
  log("info", "User abort (ESC)");
@@ -14559,6 +14643,11 @@ var AutoResumePlugin = async (ctx, options) => {
14559
14643
  if (w) {
14560
14644
  w.pendingTools = 0;
14561
14645
  w.pendingCommands = 0;
14646
+ if (!w.oocLocked && OOC_ERROR_RE.test(errorMessage ?? "")) {
14647
+ w.oocLocked = true;
14648
+ w.oocLockReason = (errorMessage ?? "").slice(0, 200);
14649
+ await log("warn", `${short(sid)} - OOC error latched, halting recovery (reason: ${w.oocLockReason})`);
14650
+ }
14562
14651
  }
14563
14652
  }
14564
14653
  break;
@@ -14571,7 +14660,11 @@ var AutoResumePlugin = async (ctx, options) => {
14571
14660
  if (w.pendingRecovery) {
14572
14661
  dbg(`Pending recovery cleared on ${short(sid)}: reason=user-command`);
14573
14662
  }
14574
- resetBusyFlags(w);
14663
+ if (w.continuing) {
14664
+ dbg(`command on ${short(sid)} via recovery prompt - preserving resumeAttempts=${w.resumeAttempts} gaveUp=${w.gaveUp}`);
14665
+ } else {
14666
+ resetBusyFlags(w);
14667
+ }
14575
14668
  w.pendingCommands = Math.max(0, w.pendingCommands - 1);
14576
14669
  w.lastActivityAt = Date.now();
14577
14670
  }
@@ -14659,6 +14752,11 @@ ${reminder}`;
14659
14752
  w.lastActivityAt = Date.now();
14660
14753
  if (w.continuing)
14661
14754
  return;
14755
+ if (w.oocLocked) {
14756
+ w.oocLocked = false;
14757
+ w.oocLockReason = null;
14758
+ await log("info", `${short(sid)} - genuine user message, clearing oocLocked`);
14759
+ }
14662
14760
  if (w.userCancelled || w.completionSignaled) {
14663
14761
  w.userCancelled = false;
14664
14762
  w.completionSignaled = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-auto-resume",
3
- "version": "1.1.18",
3
+ "version": "1.1.20",
4
4
  "description": "OpenCode plugin that automatically resumes stalled LLM sessions when thinking/streaming freezes mid-generation.",
5
5
  "keywords": [
6
6
  "opencode",