opencode-auto-resume 1.2.2 → 1.2.4

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 CHANGED
@@ -238,6 +238,10 @@ _Motivated by:_
238
238
  - [#33050](https://github.com/anomalyco/opencode/issues/33050) — orphaned sessions continue looping after abort, causing sustained high CPU
239
239
  - [#32335](https://github.com/anomalyco/opencode/issues/32335) — opencode run processes don't exit after completing scheduled work, causing memory leak
240
240
 
241
+ #### Subagent dead response
242
+
243
+ A child that finishes idle with a provider error, or with an empty finish (no text, no tool call), leaves its parent waiting on a result that will never arrive. The plugin detects the dead child on its own idle event and sends one targeted recovery prompt to that child, then hands back to the parent-side orphan watch. This applies to both builds: v1 reads the child's message history through the SDK; v2 reads the graph messages on the child session. Handling is suppressed when the child was cancelled by the user, already signalled completion, gave up, is waiting on a permission, is compacting, is mid-recovery, or when the plugin itself aborted it. Only the current turn counts — an error or empty finish before a newer user message is history, not a dead child. One prompt per episode; a genuine new user message re-arms it. Log line: `subagent error <reason> — sending one recovery prompt` or `subagent empty finish=<reason> — sending one recovery prompt` (v1 prefixes the short id with ` - `).
244
+
241
245
  #### Subagent stuck detection
242
246
 
243
247
  Detects when a subagent hasn't received new text for >1 minute (or >3 minutes if a tool call is in progress). If stuck, sends a recovery prompt to the subagent before triggering abort+resume on the parent. This avoids killing a parent that merely has a slow child.
@@ -404,7 +408,7 @@ All recovery paths fall into three families. Which family fires determines what
404
408
 
405
409
  Fire only after OpenCode reports the session **idle** — the runner has exited. The prompt starts a new run.
406
410
 
407
- Paths: todo nudges, tool-call-as-text recovery, thinking-tool recovery, action-intent nudge, ready-to-continue, done-claim verification, streaming-failure recovery, silent dead-stream recovery, context-saturation routing (magic-context-gated; parent sessions use `session.command`, subagents use opt-in native `session.summarize`).
411
+ Paths: todo nudges, tool-call-as-text recovery, thinking-tool recovery, action-intent nudge, ready-to-continue, done-claim verification, streaming-failure recovery, silent dead-stream recovery, subagent dead-response recovery, context-saturation routing (magic-context-gated; parent sessions use `session.command`, subagents use opt-in native `session.summarize`).
408
412
 
409
413
  ### 2. Busy-silence continue (stream stall)
410
414
 
@@ -424,7 +428,6 @@ Aborts the active run, then continues. The only family that can unblock a live r
424
428
 
425
429
  Paths: orphan parent, subagent stuck (parent side), hallucination loop, streaming-failure escalation. All aborts pass the active-tool guards first, and plugin-initiated aborts are marked so they are not mistaken for user ESC (`session.error` → `MessageAbortedError` race).
426
430
 
427
- <<<<<<< HEAD
428
431
  ## Architecture
429
432
 
430
433
 
@@ -434,6 +437,8 @@ To verify the plugin is loaded, run `/status` inside OpenCode — it lists all l
434
437
 
435
438
  On v2 a clean load writes nothing at startup: warnings and recovery lines go to the server process output, not a file, and there is no ready banner. To confirm the config reached the plugin, trigger any recovery — every action is logged to the process output.
436
439
 
440
+ On v1 every startup line carries the loaded build version (`opencode-auto-resume ready (vX.Y.Z)`, `opencode-auto-resume config OK (vX.Y.Z)`), and diagnostics are prefixed `[auto-resume vX.Y.Z]`. opencode 1.19.5 and later run two plugin loaders: the v1 `server()` loader and the v2 config-plugin `setup()` loader. The setup context has service domains only — no event stream and no client — so the plugin cannot recover sessions from it. The dispatcher detects that shape, logs `opencode v2 config-plugin API detected ... this setup() call is inert`, and leaves the v1 copy loaded through `server()` active. The `ready (vX.Y.Z)` line is the proof the active copy is running.
441
+
437
442
  | Problem | Solution |
438
443
  |---|---|
439
444
  | Resumes after ESC | Increase `gracePeriodMs` to `5000` |
@@ -446,6 +451,7 @@ On v2 a clean load writes nothing at startup: warnings and recovery lines go to
446
451
  | Long-running tool killed | Should not happen — active-tool guard prevents it. Report a bug. |
447
452
  | No plugin activity in the log | The entry is not in a location opencode scans, or not listed in `plugins`; fix the config and restart opencode. |
448
453
  | Log says `ctx.app.version is missing` (or `ctx.serverUrl is missing` on v1) | The build was loaded on the wrong server generation. The plugin disables itself with this diagnostic instead of crashing. Update the npm package (the universal entry auto-selects the branch by context shape) or match the build file to the server. |
454
+ | Log says `opencode v2 config-plugin API detected` or `this setup() call is inert` | Expected on opencode 1.19.5+. Its second loader calls `setup()` with a service-only context; the v1 `server()` copy still handles recovery. Confirm the `opencode-auto-resume ready (vX.Y.Z)` startup line. If you instead see `unrecognised host context`, the host shape is unsupported. |
449
455
  | Never recovers a parent waiting on a subagent | Intentional: parent sessions blocked on a running subagent are left alone. |
450
456
  | Never recovers while a permission dialog is open | Intentional: recovery is held until the permission is answered. |
451
457
  | Recovers but you don't see a notice (v2) | Your model/provider may reject `session.synthetic()`; the plugin falls back to `session.prompt()` (no TUI banner). |
package/dist/index.js CHANGED
@@ -12343,7 +12343,56 @@ function tool(input) {
12343
12343
  return input;
12344
12344
  }
12345
12345
  tool.schema = exports_external;
12346
+ // package.json
12347
+ var package_default = {
12348
+ name: "opencode-auto-resume",
12349
+ version: "1.2.4",
12350
+ description: "OpenCode plugin that automatically resumes stalled LLM sessions when thinking/streaming freezes mid-generation.",
12351
+ keywords: [
12352
+ "opencode",
12353
+ "plugin",
12354
+ "auto-resume",
12355
+ "stall-detection",
12356
+ "llm"
12357
+ ],
12358
+ homepage: "https://github.com/Mte90/opencode-auto-resume#readme",
12359
+ bugs: {
12360
+ url: "https://github.com/Mte90/opencode-auto-resume/issues"
12361
+ },
12362
+ repository: {
12363
+ type: "git",
12364
+ url: "git+https://github.com/Mte90/opencode-auto-resume.git"
12365
+ },
12366
+ license: "GPL-3.0-or-later",
12367
+ author: 'Daniele "mte90" Scasciafratte',
12368
+ type: "module",
12369
+ main: "dist/index.js",
12370
+ scripts: {
12371
+ build: "bun run build:v1 && bun run build:v2",
12372
+ "build:v1": "bun build src/dispatch.ts --outfile dist/index.js --target bun",
12373
+ "build:v2": "bun build src/v2/index.ts --outfile dist/v2/index.js --target bun",
12374
+ dev: "bun build src/dispatch.ts --outfile dist/index.js --target bun --watch",
12375
+ test: "bun run build && bun test",
12376
+ prepublishOnly: "bun run build"
12377
+ },
12378
+ files: [
12379
+ "dist/index.js",
12380
+ "dist/v2/index.js",
12381
+ "README.md",
12382
+ "LICENSE"
12383
+ ],
12384
+ dependencies: {
12385
+ "@opencode-ai/plugin": "latest"
12386
+ },
12387
+ devDependencies: {
12388
+ "@opencode-ai/sdk": "latest",
12389
+ "@types/bun": "latest",
12390
+ typescript: "latest"
12391
+ }
12392
+ };
12393
+
12346
12394
  // src/index.ts
12395
+ var PLUGIN_VERSION = package_default.version;
12347
12396
  var DEFAULT_CHUNK_TIMEOUT_MS = 180000;
12348
12397
  var DEFAULT_CHECK_INTERVAL_MS = 5000;
12349
12398
  var DEFAULT_DISCOVERY_DELAY_MS = 5000;
@@ -12682,7 +12731,7 @@ Please continue working on ${thisWord} ${taskWord}.`;
12682
12731
  }
12683
12732
  var AutoResumePlugin = async (ctx, options) => {
12684
12733
  if (!ctx.serverUrl) {
12685
- console.warn("[auto-resume] this looks like the opencode v1 build running on the wrong server: ctx.serverUrl is missing. The v1 bundle requires opencode v1 \u2014 update the plugin package (the npm build auto-selects per server) or load the build matching your server; see the README on npm (opencode-auto-resume).");
12734
+ console.warn(`[auto-resume v${PLUGIN_VERSION}] this looks like the opencode v1 build running on the wrong server: ctx.serverUrl is missing. The v1 bundle requires opencode v1 \u2014 update the plugin package (the npm build auto-selects per server) or load the build matching your server; see the README on npm (opencode-auto-resume).`);
12686
12735
  return {
12687
12736
  event: async () => {},
12688
12737
  config: async () => {}
@@ -12837,6 +12886,7 @@ var AutoResumePlugin = async (ctx, options) => {
12837
12886
  contextWrapupAttempts: 0,
12838
12887
  toolLoopAttempts: 0,
12839
12888
  isSubagent: false,
12889
+ subagentRecoveryTried: false,
12840
12890
  completionSignaled: false,
12841
12891
  todoNudgeAttempts: 0,
12842
12892
  taskCompleteOverrides: 0,
@@ -12849,6 +12899,7 @@ var AutoResumePlugin = async (ctx, options) => {
12849
12899
  pendingRecoveryReason: null,
12850
12900
  pendingRecoveryAt: 0,
12851
12901
  recoveryAttempts: 0,
12902
+ recoveryContinueInFlight: false,
12852
12903
  watchdogRetryGuard: false,
12853
12904
  unknownToolErrors: new Map,
12854
12905
  unknownToolSuggestionSent: false,
@@ -13092,6 +13143,7 @@ var AutoResumePlugin = async (ctx, options) => {
13092
13143
  const retryBackoffMs = backoffMs(w.recoveryAttempts, baseBackoffMs, maxBackoffMs);
13093
13144
  await log("info", `Retrying recovery on ${short(sid)}: attempt=${w.recoveryAttempts}, backoffMs=${retryBackoffMs}`);
13094
13145
  dbg(`Retrying recovery on ${short(sid)}: recoveryAttempts=${w.recoveryAttempts}, backoffMs=${retryBackoffMs}, pendingRecoveryReason=${w.pendingRecoveryReason}`);
13146
+ w.recoveryContinueInFlight = true;
13095
13147
  try {
13096
13148
  await sendContinuePrompt(sid, continuePrompt, w);
13097
13149
  } catch (err) {
@@ -13259,6 +13311,40 @@ var AutoResumePlugin = async (ctx, options) => {
13259
13311
  return;
13260
13312
  return msg.role ?? msg.info?.role;
13261
13313
  }
13314
+ function getSubagentResponseFailure(messages) {
13315
+ let lastUser = -1;
13316
+ for (let i = messages.length - 1;i >= 0; i--) {
13317
+ if (roleOf(messages[i]) === "user") {
13318
+ lastUser = i;
13319
+ break;
13320
+ }
13321
+ }
13322
+ const turn = lastUser >= 0 ? messages.slice(lastUser + 1) : messages;
13323
+ const error = getLastAssistantError(turn);
13324
+ if (error) {
13325
+ const detail = [error.name, error.message].filter(Boolean).join(": ") || "error";
13326
+ return { kind: "error", detail };
13327
+ }
13328
+ for (let i = turn.length - 1;i >= 0; i--) {
13329
+ const msg = turn[i];
13330
+ if (roleOf(msg) !== "assistant")
13331
+ continue;
13332
+ const info = msg.info;
13333
+ const finish = msg.finish ?? info?.finish ?? info?.finishReason;
13334
+ if (!finish)
13335
+ return null;
13336
+ const parts = msg.parts ?? [];
13337
+ const hasText = parts.some((p) => p.type === "text" && typeof p.text === "string" && p.text.length > 0);
13338
+ const hasToolCall = parts.some((p) => {
13339
+ const type = typeof p.type === "string" ? p.type : "";
13340
+ return type.startsWith("tool");
13341
+ }) || msg.toolCall !== undefined || (msg.tool_calls?.length ?? 0) > 0;
13342
+ if (hasText || hasToolCall)
13343
+ return null;
13344
+ return { kind: "empty", detail: finish };
13345
+ }
13346
+ return null;
13347
+ }
13262
13348
  function messageTimeOf(msg) {
13263
13349
  if (!msg)
13264
13350
  return;
@@ -13558,15 +13644,15 @@ var AutoResumePlugin = async (ctx, options) => {
13558
13644
  w.pendingRecovery = false;
13559
13645
  w.pendingRecoveryReason = null;
13560
13646
  w.pendingRecoveryAt = 0;
13647
+ w.subagentRecoveryTried = false;
13561
13648
  w.recoveryAttempts = 0;
13649
+ w.recoveryContinueInFlight = false;
13562
13650
  w.watchdogRetryGuard = false;
13563
13651
  }
13564
13652
  function resetBusyFlags(w) {
13565
- w.resumeAttempts = 0;
13566
13653
  w.lastRetryAt = 0;
13567
13654
  w.pendingTools = 0;
13568
13655
  w.pendingCommands = 0;
13569
- w.gaveUp = false;
13570
13656
  w.orphanWatchStartAt = null;
13571
13657
  w.aborting = false;
13572
13658
  w.toolTextRecovered = false;
@@ -13581,7 +13667,7 @@ var AutoResumePlugin = async (ctx, options) => {
13581
13667
  w.pendingRecovery = false;
13582
13668
  w.pendingRecoveryReason = null;
13583
13669
  w.pendingRecoveryAt = 0;
13584
- w.recoveryAttempts = 0;
13670
+ w.recoveryContinueInFlight = false;
13585
13671
  w.watchdogRetryGuard = false;
13586
13672
  if (w.toolTextTimer) {
13587
13673
  clearTimeout(w.toolTextTimer);
@@ -13974,6 +14060,7 @@ var AutoResumePlugin = async (ctx, options) => {
13974
14060
  return false;
13975
14061
  if (w.oocLocked)
13976
14062
  return false;
14063
+ w.aborting = true;
13977
14064
  const idleSec = Math.round((Date.now() - (w.orphanWatchStartAt ?? w.lastActivityAt)) / 1000);
13978
14065
  await log("info", `Abort+Resume on ${short(sid)} (${idleSec}s idle). Aborting...`);
13979
14066
  w.pluginAbortInFlight = true;
@@ -13999,6 +14086,7 @@ var AutoResumePlugin = async (ctx, options) => {
13999
14086
  return false;
14000
14087
  }
14001
14088
  try {
14089
+ w.recoveryContinueInFlight = false;
14002
14090
  await sendContinuePrompt(sid, continuePrompt, w);
14003
14091
  await log("info", `${short(sid)} - abort+continue done`);
14004
14092
  w.orphanWatchStartAt = null;
@@ -14234,6 +14322,7 @@ var AutoResumePlugin = async (ctx, options) => {
14234
14322
  dbg(`Recovery timing on ${short(sid)}: detectionToAttemptMs=${Date.now() - w.pendingRecoveryAt}`);
14235
14323
  w.recoveryAttempts++;
14236
14324
  dbg(`State transition on ${short(sid)}: recoveryAttempts=${w.recoveryAttempts - 1} -> ${w.recoveryAttempts}`);
14325
+ w.recoveryContinueInFlight = true;
14237
14326
  try {
14238
14327
  await sendContinuePrompt(sid, continuePrompt, w);
14239
14328
  } catch (err) {
@@ -14328,6 +14417,11 @@ var AutoResumePlugin = async (ctx, options) => {
14328
14417
  if (w.pendingRecovery) {
14329
14418
  dbg(`Pending recovery cleared on ${short(sid)}: reason=session-busy`);
14330
14419
  }
14420
+ if (w.recoveryContinueInFlight) {
14421
+ w.recoveryAttempts = 0;
14422
+ w.recoveryContinueInFlight = false;
14423
+ dbg(`Recovery succeeded on ${short(sid)}: recoveryAttempts reset to 0`);
14424
+ }
14331
14425
  if (w.continuing) {
14332
14426
  dbg(`busy on ${short(sid)} via recovery prompt - preserving resumeAttempts=${w.resumeAttempts} gaveUp=${w.gaveUp}`);
14333
14427
  } else {
@@ -14388,6 +14482,21 @@ var AutoResumePlugin = async (ctx, options) => {
14388
14482
  dbg(`session.idle sid=${short(sid)}: context-saturation check error: ${errMsg}`);
14389
14483
  }
14390
14484
  }
14485
+ if (w.isSubagent && !w.subagentRecoveryTried && !w.userCancelled && !w.completionSignaled && !w.aborting && !w.gaveUp && !w.pendingRecovery && !w.continuing) {
14486
+ try {
14487
+ const subMessages = await getSessionMessages(sid);
14488
+ const failure = getSubagentResponseFailure(subMessages);
14489
+ if (failure && !hasPendingUserInput(subMessages) && !userRecentlyActive(w)) {
14490
+ w.subagentRecoveryTried = true;
14491
+ const reason = failure.kind === "error" ? `error ${failure.detail}` : `empty finish=${failure.detail}`;
14492
+ await log("info", `${short(sid)} - subagent ${reason} \u2014 sending one recovery prompt`);
14493
+ await recoverSubagent(sid);
14494
+ }
14495
+ } catch (e) {
14496
+ const errMsg = e instanceof Error ? e.message : String(e);
14497
+ dbg(`session.idle sid=${short(sid)}: subagent dead-response check error: ${errMsg}`);
14498
+ }
14499
+ }
14391
14500
  if (!w.isSubagent) {
14392
14501
  let awaitingUserInput = false;
14393
14502
  try {
@@ -14654,6 +14763,7 @@ var AutoResumePlugin = async (ctx, options) => {
14654
14763
  if (w) {
14655
14764
  w.doneClaimNoTodosAttempts = 0;
14656
14765
  w.lastUserMessageAt = Date.now();
14766
+ w.subagentRecoveryTried = false;
14657
14767
  w.unknownToolErrors.clear();
14658
14768
  w.unknownToolSuggestionSent = false;
14659
14769
  }
@@ -14820,7 +14930,7 @@ ${reminder}`;
14820
14930
  event: async ({ event }) => {
14821
14931
  if (!initialised) {
14822
14932
  initialised = true;
14823
- log("info", `opencode-auto-resume ready. timeout=${chunkTimeoutMs}ms, orphan=${subagentWaitMs}ms, loop=${loopMaxContinues}x/${loopWindowMs / 1000}s`);
14933
+ log("info", `opencode-auto-resume ready (v${PLUGIN_VERSION}). timeout=${chunkTimeoutMs}ms, orphan=${subagentWaitMs}ms, loop=${loopMaxContinues}x/${loopWindowMs / 1000}s`);
14824
14934
  }
14825
14935
  handleEvent(event).catch((e) => {
14826
14936
  const msg = e instanceof Error ? e.message : String(e);
@@ -14829,7 +14939,7 @@ ${reminder}`;
14829
14939
  });
14830
14940
  },
14831
14941
  config: async () => {
14832
- log("info", `opencode-auto-resume config OK`);
14942
+ log("info", `opencode-auto-resume config OK (v${PLUGIN_VERSION})`);
14833
14943
  },
14834
14944
  tool: {
14835
14945
  task_complete: taskCompleteTool
@@ -14916,6 +15026,7 @@ import { dirname, join } from "path";
14916
15026
  import { fileURLToPath } from "url";
14917
15027
  import { readFile } from "fs/promises";
14918
15028
  var define = (plugin) => plugin;
15029
+ var PLUGIN_VERSION2 = package_default.version;
14919
15030
  async function configMentionsTaskComplete(cfgPath) {
14920
15031
  try {
14921
15032
  const raw = await readFile(cfgPath, "utf8");
@@ -15684,7 +15795,7 @@ var v2_default = define({
15684
15795
  log("warn", `ignoring unrecognised option(s): ${unknownOptions.join(", ")} \u2014 ` + `this v2 build does not read them. See the README on npm (opencode-auto-resume).`);
15685
15796
  }
15686
15797
  if (typeof ctx.app?.version !== "string") {
15687
- log("warn", "this looks like the opencode v1 build running on the wrong server: ctx.app.version is missing. The v2 bundle requires opencode v2 \u2014 update the plugin package (the npm build auto-selects per server) or load the build matching your server; see the README on npm (opencode-auto-resume).");
15798
+ log("warn", `v${PLUGIN_VERSION2} \u2014 this looks like the opencode v1 build running on the wrong server: ctx.app.version is missing. The v2 bundle requires opencode v2 \u2014 update the plugin package (the npm build auto-selects per server) or load the build matching your server; see the README on npm (opencode-auto-resume).`);
15688
15799
  return () => {};
15689
15800
  }
15690
15801
  const gatedInUse = Object.keys(opts).filter((key) => FEATURE_GATED_OPTIONS.includes(key));
@@ -15737,6 +15848,7 @@ var v2_default = define({
15737
15848
  let w = sessions.get(sid);
15738
15849
  if (w) {
15739
15850
  w.emptyIdleNudged ??= false;
15851
+ w.subagentRecoveryTried ??= false;
15740
15852
  w.workRetries ??= 0;
15741
15853
  w.recoverGeneration ??= 0;
15742
15854
  w.lifetimeContinues ??= 0;
@@ -15796,6 +15908,7 @@ var v2_default = define({
15796
15908
  completionHard: false,
15797
15909
  latchReopened: false,
15798
15910
  emptyIdleNudged: false,
15911
+ subagentRecoveryTried: false,
15799
15912
  todos: [],
15800
15913
  todosFetchedAt: 0,
15801
15914
  taskCompleteSignals: 0,
@@ -16911,6 +17024,48 @@ ${suffix}`).slice(0, 2000);
16911
17024
  }
16912
17025
  return null;
16913
17026
  }
17027
+ function lastAssistantError(messages) {
17028
+ for (let i = messages.length - 1;i >= 0; i--) {
17029
+ const msg = messages[i];
17030
+ if (msgRole(msg) !== "assistant")
17031
+ continue;
17032
+ const err = msg.error ?? msg.info?.error;
17033
+ if (err === undefined || err === null)
17034
+ return null;
17035
+ if (typeof err === "object") {
17036
+ const e = err;
17037
+ return String(e.type ?? e.data?.message ?? e.message ?? "error");
17038
+ }
17039
+ return String(err);
17040
+ }
17041
+ return null;
17042
+ }
17043
+ async function recoverSubagentResponse(sid, w, messages) {
17044
+ if (w.subagentRecoveryTried)
17045
+ return false;
17046
+ if (!await isSubAgentSession(sid))
17047
+ return false;
17048
+ let lastUser = -1;
17049
+ for (let i = messages.length - 1;i >= 0; i--) {
17050
+ if (msgRole(messages[i]) === "user") {
17051
+ lastUser = i;
17052
+ break;
17053
+ }
17054
+ }
17055
+ const turn = lastUser >= 0 ? messages.slice(lastUser + 1) : messages;
17056
+ const error = lastAssistantError(turn);
17057
+ const dead = lastSilentDeadStream(turn);
17058
+ if (!error && !dead)
17059
+ return false;
17060
+ if (w.userCancelled || w.gaveUp || w.completionSignaled || w.permissionPending || w.compacting || w.recovering || selfAbortActive(w)) {
17061
+ return false;
17062
+ }
17063
+ w.subagentRecoveryTried = true;
17064
+ const reason = error ? `error ${error}` : `empty finish=${dead?.finish ?? "unknown"}`;
17065
+ log("info", `${short(sid)} subagent ${reason} \u2014 sending one recovery prompt`);
17066
+ await recoverStuckSubagent(sid);
17067
+ return true;
17068
+ }
16914
17069
  async function recoverSilentDeadStream(sid, messages) {
16915
17070
  const dead = lastSilentDeadStream(messages);
16916
17071
  if (!dead)
@@ -17143,6 +17298,7 @@ ${suffix}`).slice(0, 2000);
17143
17298
  }
17144
17299
  w.gaveUp = false;
17145
17300
  w.emptyIdleNudged = false;
17301
+ w.subagentRecoveryTried = false;
17146
17302
  }
17147
17303
  let cachedToolIds = null;
17148
17304
  let cachedToolIdsAt = 0;
@@ -17479,6 +17635,8 @@ ${reminder}`;
17479
17635
  const w = ensureWatch(sid);
17480
17636
  const messages = await loadMessages(sid);
17481
17637
  noteInboundUserMessage(sid, w, messages);
17638
+ if (await recoverSubagentResponse(sid, w, messages))
17639
+ return;
17482
17640
  if (await recoverSilentDeadStream(sid, messages))
17483
17641
  return;
17484
17642
  if (await checkForStuckToolCalls(sid, w))
@@ -18929,7 +19087,9 @@ ${reminder}`;
18929
19087
  });
18930
19088
 
18931
19089
  // src/dispatch.ts
19090
+ var PLUGIN_VERSION3 = package_default.version;
18932
19091
  var noopDispose = () => {};
19092
+ var isV2ServiceContext = (ctx) => typeof ctx?.plugin?.add === "function" && typeof ctx?.agent?.transform === "function" && typeof ctx?.skill?.transform === "function";
18933
19093
  var dispatcher = {
18934
19094
  id: "auto-resume.v2",
18935
19095
  server: AutoResumePlugin,
@@ -18942,7 +19102,11 @@ var dispatcher = {
18942
19102
  if (ctx?.serverUrl || ctx?.client?.session) {
18943
19103
  return AutoResumePlugin(ctx, ctx.options);
18944
19104
  }
18945
- console.warn("[auto-resume] unrecognised host context \u2014 no ctx.app.version (opencode v2), no ctx.event.subscribe (v2 loader), no ctx.serverUrl/client (opencode v1). Plugin disabled; see the README on npm (opencode-auto-resume).");
19105
+ if (isV2ServiceContext(ctx)) {
19106
+ console.log(`[auto-resume v${PLUGIN_VERSION3}] opencode v2 config-plugin API detected; auto-resume runs through the v1 server() loader on hosts that provide it \u2014 this setup() call is inert.`);
19107
+ return noopDispose;
19108
+ }
19109
+ console.warn(`[auto-resume v${PLUGIN_VERSION3}] unrecognised host context \u2014 no ctx.app.version (opencode v2), no ctx.event.subscribe (v2 loader), no ctx.serverUrl/client (opencode v1). Plugin disabled; see the README on npm (opencode-auto-resume).`);
18946
19110
  return noopDispose;
18947
19111
  }
18948
19112
  };
package/dist/v2/index.js CHANGED
@@ -4,7 +4,57 @@ import { readFileSync, mkdirSync, writeFileSync } from "fs";
4
4
  import { dirname, join } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { readFile } from "fs/promises";
7
+ // package.json
8
+ var package_default = {
9
+ name: "opencode-auto-resume",
10
+ version: "1.2.4",
11
+ description: "OpenCode plugin that automatically resumes stalled LLM sessions when thinking/streaming freezes mid-generation.",
12
+ keywords: [
13
+ "opencode",
14
+ "plugin",
15
+ "auto-resume",
16
+ "stall-detection",
17
+ "llm"
18
+ ],
19
+ homepage: "https://github.com/Mte90/opencode-auto-resume#readme",
20
+ bugs: {
21
+ url: "https://github.com/Mte90/opencode-auto-resume/issues"
22
+ },
23
+ repository: {
24
+ type: "git",
25
+ url: "git+https://github.com/Mte90/opencode-auto-resume.git"
26
+ },
27
+ license: "GPL-3.0-or-later",
28
+ author: 'Daniele "mte90" Scasciafratte',
29
+ type: "module",
30
+ main: "dist/index.js",
31
+ scripts: {
32
+ build: "bun run build:v1 && bun run build:v2",
33
+ "build:v1": "bun build src/dispatch.ts --outfile dist/index.js --target bun",
34
+ "build:v2": "bun build src/v2/index.ts --outfile dist/v2/index.js --target bun",
35
+ dev: "bun build src/dispatch.ts --outfile dist/index.js --target bun --watch",
36
+ test: "bun run build && bun test",
37
+ prepublishOnly: "bun run build"
38
+ },
39
+ files: [
40
+ "dist/index.js",
41
+ "dist/v2/index.js",
42
+ "README.md",
43
+ "LICENSE"
44
+ ],
45
+ dependencies: {
46
+ "@opencode-ai/plugin": "latest"
47
+ },
48
+ devDependencies: {
49
+ "@opencode-ai/sdk": "latest",
50
+ "@types/bun": "latest",
51
+ typescript: "latest"
52
+ }
53
+ };
54
+
55
+ // src/v2/index.ts
7
56
  var define = (plugin) => plugin;
57
+ var PLUGIN_VERSION = package_default.version;
8
58
  async function configMentionsTaskComplete(cfgPath) {
9
59
  try {
10
60
  const raw = await readFile(cfgPath, "utf8");
@@ -773,7 +823,7 @@ var v2_default = define({
773
823
  log("warn", `ignoring unrecognised option(s): ${unknownOptions.join(", ")} \u2014 ` + `this v2 build does not read them. See the README on npm (opencode-auto-resume).`);
774
824
  }
775
825
  if (typeof ctx.app?.version !== "string") {
776
- log("warn", "this looks like the opencode v1 build running on the wrong server: ctx.app.version is missing. The v2 bundle requires opencode v2 \u2014 update the plugin package (the npm build auto-selects per server) or load the build matching your server; see the README on npm (opencode-auto-resume).");
826
+ log("warn", `v${PLUGIN_VERSION} \u2014 this looks like the opencode v1 build running on the wrong server: ctx.app.version is missing. The v2 bundle requires opencode v2 \u2014 update the plugin package (the npm build auto-selects per server) or load the build matching your server; see the README on npm (opencode-auto-resume).`);
777
827
  return () => {};
778
828
  }
779
829
  const gatedInUse = Object.keys(opts).filter((key) => FEATURE_GATED_OPTIONS.includes(key));
@@ -826,6 +876,7 @@ var v2_default = define({
826
876
  let w = sessions.get(sid);
827
877
  if (w) {
828
878
  w.emptyIdleNudged ??= false;
879
+ w.subagentRecoveryTried ??= false;
829
880
  w.workRetries ??= 0;
830
881
  w.recoverGeneration ??= 0;
831
882
  w.lifetimeContinues ??= 0;
@@ -885,6 +936,7 @@ var v2_default = define({
885
936
  completionHard: false,
886
937
  latchReopened: false,
887
938
  emptyIdleNudged: false,
939
+ subagentRecoveryTried: false,
888
940
  todos: [],
889
941
  todosFetchedAt: 0,
890
942
  taskCompleteSignals: 0,
@@ -2000,6 +2052,48 @@ ${suffix}`).slice(0, 2000);
2000
2052
  }
2001
2053
  return null;
2002
2054
  }
2055
+ function lastAssistantError(messages) {
2056
+ for (let i = messages.length - 1;i >= 0; i--) {
2057
+ const msg = messages[i];
2058
+ if (msgRole(msg) !== "assistant")
2059
+ continue;
2060
+ const err = msg.error ?? msg.info?.error;
2061
+ if (err === undefined || err === null)
2062
+ return null;
2063
+ if (typeof err === "object") {
2064
+ const e = err;
2065
+ return String(e.type ?? e.data?.message ?? e.message ?? "error");
2066
+ }
2067
+ return String(err);
2068
+ }
2069
+ return null;
2070
+ }
2071
+ async function recoverSubagentResponse(sid, w, messages) {
2072
+ if (w.subagentRecoveryTried)
2073
+ return false;
2074
+ if (!await isSubAgentSession(sid))
2075
+ return false;
2076
+ let lastUser = -1;
2077
+ for (let i = messages.length - 1;i >= 0; i--) {
2078
+ if (msgRole(messages[i]) === "user") {
2079
+ lastUser = i;
2080
+ break;
2081
+ }
2082
+ }
2083
+ const turn = lastUser >= 0 ? messages.slice(lastUser + 1) : messages;
2084
+ const error = lastAssistantError(turn);
2085
+ const dead = lastSilentDeadStream(turn);
2086
+ if (!error && !dead)
2087
+ return false;
2088
+ if (w.userCancelled || w.gaveUp || w.completionSignaled || w.permissionPending || w.compacting || w.recovering || selfAbortActive(w)) {
2089
+ return false;
2090
+ }
2091
+ w.subagentRecoveryTried = true;
2092
+ const reason = error ? `error ${error}` : `empty finish=${dead?.finish ?? "unknown"}`;
2093
+ log("info", `${short(sid)} subagent ${reason} \u2014 sending one recovery prompt`);
2094
+ await recoverStuckSubagent(sid);
2095
+ return true;
2096
+ }
2003
2097
  async function recoverSilentDeadStream(sid, messages) {
2004
2098
  const dead = lastSilentDeadStream(messages);
2005
2099
  if (!dead)
@@ -2232,6 +2326,7 @@ ${suffix}`).slice(0, 2000);
2232
2326
  }
2233
2327
  w.gaveUp = false;
2234
2328
  w.emptyIdleNudged = false;
2329
+ w.subagentRecoveryTried = false;
2235
2330
  }
2236
2331
  let cachedToolIds = null;
2237
2332
  let cachedToolIdsAt = 0;
@@ -2568,6 +2663,8 @@ ${reminder}`;
2568
2663
  const w = ensureWatch(sid);
2569
2664
  const messages = await loadMessages(sid);
2570
2665
  noteInboundUserMessage(sid, w, messages);
2666
+ if (await recoverSubagentResponse(sid, w, messages))
2667
+ return;
2571
2668
  if (await recoverSilentDeadStream(sid, messages))
2572
2669
  return;
2573
2670
  if (await checkForStuckToolCalls(sid, w))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-auto-resume",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "OpenCode plugin that automatically resumes stalled LLM sessions when thinking/streaming freezes mid-generation.",
5
5
  "keywords": [
6
6
  "opencode",
@@ -21,14 +21,15 @@
21
21
  "author": "Daniele \"mte90\" Scasciafratte",
22
22
  "type": "module",
23
23
  "main": "dist/index.js",
24
- "scripts": {
24
+ "scripts": {
25
25
  "build": "bun run build:v1 && bun run build:v2",
26
26
  "build:v1": "bun build src/dispatch.ts --outfile dist/index.js --target bun",
27
27
  "build:v2": "bun build src/v2/index.ts --outfile dist/v2/index.js --target bun",
28
28
  "dev": "bun build src/dispatch.ts --outfile dist/index.js --target bun --watch",
29
29
  "test": "bun run build && bun test",
30
30
  "prepublishOnly": "bun run build"
31
- }, "files": [
31
+ },
32
+ "files": [
32
33
  "dist/index.js",
33
34
  "dist/v2/index.js",
34
35
  "README.md",