opencode-auto-resume 1.1.19 → 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 +8 -7
  2. package/dist/index.js +43 -12
  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 |
@@ -420,7 +421,7 @@ With options:
420
421
  | `silentDeadStreamMinTokens` | `200` | Min output tokens to treat a textless `finish:"unknown"` message as a dead stream |
421
422
  | `busyStallStrategy` | `"continue"` | Busy-stall response: `"continue"`, `"abort"` (abort-first), or `"off"` (disabled) |
422
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) |
423
- | `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) |
424
425
  | `subagentNativeCompactionEnabled` | `false` | Opt-in native `session.summarize()` for saturated subagent sessions (no magic-context detection required) |
425
426
 
426
427
  Message patterns are matched case-insensitively. Error names use exact match.
@@ -445,7 +446,7 @@ The plugin handles all recovery automatically — no manual intervention needed.
445
446
  | Problem | Solution |
446
447
  |---|---|
447
448
  | Resumes after ESC | Increase `gracePeriodMs` to `5000` |
448
- | Too aggressive | Increase `chunkTimeoutMs` to `60000` |
449
+ | Stall recovery too aggressive | Decrease `chunkTimeoutMs` (e.g. `45000`) |
449
450
  | Too slow to react | Decrease `checkIntervalMs` to `2000` |
450
451
  | Orphan parent not detected | Increase `subagentWaitMs` to `20000` |
451
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",
@@ -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;
@@ -12770,6 +12773,8 @@ var AutoResumePlugin = async (ctx, options) => {
12770
12773
  resumeAttempts: 0,
12771
12774
  lastRetryAt: 0,
12772
12775
  gaveUp: false,
12776
+ oocLocked: false,
12777
+ oocLockReason: null,
12773
12778
  orphanWatchStartAt: null,
12774
12779
  aborting: false,
12775
12780
  pluginAbortInFlight: false,
@@ -12946,6 +12951,10 @@ var AutoResumePlugin = async (ctx, options) => {
12946
12951
  await log("debug", `${short(sid)} - gaveUp latched, refusing further continue prompts`);
12947
12952
  return;
12948
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
+ }
12949
12958
  if (!w.continuing)
12950
12959
  dbg(`State transition on ${short(sid)}: continuing=false -> true`);
12951
12960
  w.continuing = true;
@@ -13831,14 +13840,13 @@ var AutoResumePlugin = async (ctx, options) => {
13831
13840
  await log("info", `${short(sid)} - max open-todos nudges (${maxRetries}) reached, waiting for activity`);
13832
13841
  return;
13833
13842
  }
13834
- w.todoNudgeAttempts++;
13835
13843
  } else if (isDoneClaimNoTodos) {
13836
13844
  w.doneClaimNoTodosAttempts++;
13837
13845
  } else {
13838
13846
  w.toolTextRecovered = true;
13839
13847
  w.toolTextAttempts++;
13840
13848
  }
13841
- const attemptNum = isOpenTodosReminder ? w.todoNudgeAttempts : isDoneClaimNoTodos ? w.doneClaimNoTodosAttempts : w.toolTextAttempts;
13849
+ const attemptNum = isOpenTodosReminder ? w.todoNudgeAttempts + 1 : isDoneClaimNoTodos ? w.doneClaimNoTodosAttempts : w.toolTextAttempts;
13842
13850
  await log("info", `${bestCandidate.source} detected on ${short(sid)}! ` + `Attempt ${attemptNum}/${maxRetries}. Sending recovery prompt...`);
13843
13851
  const timeSinceActivity = Date.now() - w.lastActivityAt;
13844
13852
  if (timeSinceActivity < minActivityGapMs) {
@@ -13884,6 +13892,8 @@ var AutoResumePlugin = async (ctx, options) => {
13884
13892
  return false;
13885
13893
  if (w.aborting)
13886
13894
  return false;
13895
+ if (w.oocLocked)
13896
+ return false;
13887
13897
  const idleSec = Math.round((Date.now() - (w.orphanWatchStartAt ?? w.lastActivityAt)) / 1000);
13888
13898
  await log("info", `Abort+Resume on ${short(sid)} (${idleSec}s idle). Aborting...`);
13889
13899
  w.pluginAbortInFlight = true;
@@ -13968,12 +13978,13 @@ var AutoResumePlugin = async (ctx, options) => {
13968
13978
  try {
13969
13979
  const response = await ctx.client.session.list();
13970
13980
  const list = extractMessages(response);
13981
+ const statusMap = await getSessionStatusMap();
13971
13982
  for (const s of list) {
13972
13983
  const sid = s.id;
13973
13984
  if (sid && typeof sid === "string" && sid.startsWith("ses_")) {
13974
13985
  const isNew = !sessions.has(sid);
13975
13986
  ensureWatch(sid);
13976
- const status = s.status;
13987
+ const status = s.status ?? statusMap[sid];
13977
13988
  if (status) {
13978
13989
  const w = sessions.get(sid);
13979
13990
  w.status = status;
@@ -14216,7 +14227,7 @@ var AutoResumePlugin = async (ctx, options) => {
14216
14227
  }, SESSION_DISCOVERY_INTERVAL_MS);
14217
14228
  if (discoveryTimer.unref)
14218
14229
  discoveryTimer.unref();
14219
- setTimeout(discoverSessions, 5000);
14230
+ setTimeout(discoverSessions, discoveryDelayMs);
14220
14231
  }
14221
14232
  startTimer();
14222
14233
  async function handleEvent(ev) {
@@ -14237,7 +14248,11 @@ var AutoResumePlugin = async (ctx, options) => {
14237
14248
  if (w.pendingRecovery) {
14238
14249
  dbg(`Pending recovery cleared on ${short(sid)}: reason=session-busy`);
14239
14250
  }
14240
- 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
+ }
14241
14256
  prevBusyCount = busyCount();
14242
14257
  log("debug", `${short(sid)} -> busy (${prevBusyCount})`);
14243
14258
  } else if (statusType === "interrupted") {
@@ -14379,12 +14394,14 @@ var AutoResumePlugin = async (ctx, options) => {
14379
14394
  if (isCelebration) {
14380
14395
  await log("info", `${short(sid)} - \uD83C\uDF89 detected but ${open.length} open todos remain, sending nudge`);
14381
14396
  const reminder = buildOpenTodosReminder(todos);
14382
- await tryResume(sid, w, "Idle with open todos (celebration false positive)", reminder);
14383
- w.todoNudgeAttempts++;
14397
+ const sent = await tryResume(sid, w, "Idle with open todos (celebration false positive)", reminder);
14398
+ if (sent)
14399
+ w.todoNudgeAttempts++;
14384
14400
  } else {
14385
14401
  const reminder = buildOpenTodosReminder(todos);
14386
- await tryResume(sid, w, "Idle with open todos", reminder);
14387
- w.todoNudgeAttempts++;
14402
+ const sent = await tryResume(sid, w, "Idle with open todos", reminder);
14403
+ if (sent)
14404
+ w.todoNudgeAttempts++;
14388
14405
  }
14389
14406
  }
14390
14407
  }
@@ -14626,6 +14643,11 @@ var AutoResumePlugin = async (ctx, options) => {
14626
14643
  if (w) {
14627
14644
  w.pendingTools = 0;
14628
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
+ }
14629
14651
  }
14630
14652
  }
14631
14653
  break;
@@ -14638,7 +14660,11 @@ var AutoResumePlugin = async (ctx, options) => {
14638
14660
  if (w.pendingRecovery) {
14639
14661
  dbg(`Pending recovery cleared on ${short(sid)}: reason=user-command`);
14640
14662
  }
14641
- 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
+ }
14642
14668
  w.pendingCommands = Math.max(0, w.pendingCommands - 1);
14643
14669
  w.lastActivityAt = Date.now();
14644
14670
  }
@@ -14726,6 +14752,11 @@ ${reminder}`;
14726
14752
  w.lastActivityAt = Date.now();
14727
14753
  if (w.continuing)
14728
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
+ }
14729
14760
  if (w.userCancelled || w.completionSignaled) {
14730
14761
  w.userCancelled = false;
14731
14762
  w.completionSignaled = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-auto-resume",
3
- "version": "1.1.19",
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",