opencode-auto-resume 1.2.3 → 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,
@@ -13261,6 +13311,40 @@ var AutoResumePlugin = async (ctx, options) => {
13261
13311
  return;
13262
13312
  return msg.role ?? msg.info?.role;
13263
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
+ }
13264
13348
  function messageTimeOf(msg) {
13265
13349
  if (!msg)
13266
13350
  return;
@@ -13560,6 +13644,7 @@ var AutoResumePlugin = async (ctx, options) => {
13560
13644
  w.pendingRecovery = false;
13561
13645
  w.pendingRecoveryReason = null;
13562
13646
  w.pendingRecoveryAt = 0;
13647
+ w.subagentRecoveryTried = false;
13563
13648
  w.recoveryAttempts = 0;
13564
13649
  w.recoveryContinueInFlight = false;
13565
13650
  w.watchdogRetryGuard = false;
@@ -14397,6 +14482,21 @@ var AutoResumePlugin = async (ctx, options) => {
14397
14482
  dbg(`session.idle sid=${short(sid)}: context-saturation check error: ${errMsg}`);
14398
14483
  }
14399
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
+ }
14400
14500
  if (!w.isSubagent) {
14401
14501
  let awaitingUserInput = false;
14402
14502
  try {
@@ -14663,6 +14763,7 @@ var AutoResumePlugin = async (ctx, options) => {
14663
14763
  if (w) {
14664
14764
  w.doneClaimNoTodosAttempts = 0;
14665
14765
  w.lastUserMessageAt = Date.now();
14766
+ w.subagentRecoveryTried = false;
14666
14767
  w.unknownToolErrors.clear();
14667
14768
  w.unknownToolSuggestionSent = false;
14668
14769
  }
@@ -14829,7 +14930,7 @@ ${reminder}`;
14829
14930
  event: async ({ event }) => {
14830
14931
  if (!initialised) {
14831
14932
  initialised = true;
14832
- 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`);
14833
14934
  }
14834
14935
  handleEvent(event).catch((e) => {
14835
14936
  const msg = e instanceof Error ? e.message : String(e);
@@ -14838,7 +14939,7 @@ ${reminder}`;
14838
14939
  });
14839
14940
  },
14840
14941
  config: async () => {
14841
- log("info", `opencode-auto-resume config OK`);
14942
+ log("info", `opencode-auto-resume config OK (v${PLUGIN_VERSION})`);
14842
14943
  },
14843
14944
  tool: {
14844
14945
  task_complete: taskCompleteTool
@@ -14925,6 +15026,7 @@ import { dirname, join } from "path";
14925
15026
  import { fileURLToPath } from "url";
14926
15027
  import { readFile } from "fs/promises";
14927
15028
  var define = (plugin) => plugin;
15029
+ var PLUGIN_VERSION2 = package_default.version;
14928
15030
  async function configMentionsTaskComplete(cfgPath) {
14929
15031
  try {
14930
15032
  const raw = await readFile(cfgPath, "utf8");
@@ -15693,7 +15795,7 @@ var v2_default = define({
15693
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).`);
15694
15796
  }
15695
15797
  if (typeof ctx.app?.version !== "string") {
15696
- 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).`);
15697
15799
  return () => {};
15698
15800
  }
15699
15801
  const gatedInUse = Object.keys(opts).filter((key) => FEATURE_GATED_OPTIONS.includes(key));
@@ -15746,6 +15848,7 @@ var v2_default = define({
15746
15848
  let w = sessions.get(sid);
15747
15849
  if (w) {
15748
15850
  w.emptyIdleNudged ??= false;
15851
+ w.subagentRecoveryTried ??= false;
15749
15852
  w.workRetries ??= 0;
15750
15853
  w.recoverGeneration ??= 0;
15751
15854
  w.lifetimeContinues ??= 0;
@@ -15805,6 +15908,7 @@ var v2_default = define({
15805
15908
  completionHard: false,
15806
15909
  latchReopened: false,
15807
15910
  emptyIdleNudged: false,
15911
+ subagentRecoveryTried: false,
15808
15912
  todos: [],
15809
15913
  todosFetchedAt: 0,
15810
15914
  taskCompleteSignals: 0,
@@ -16920,6 +17024,48 @@ ${suffix}`).slice(0, 2000);
16920
17024
  }
16921
17025
  return null;
16922
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
+ }
16923
17069
  async function recoverSilentDeadStream(sid, messages) {
16924
17070
  const dead = lastSilentDeadStream(messages);
16925
17071
  if (!dead)
@@ -17152,6 +17298,7 @@ ${suffix}`).slice(0, 2000);
17152
17298
  }
17153
17299
  w.gaveUp = false;
17154
17300
  w.emptyIdleNudged = false;
17301
+ w.subagentRecoveryTried = false;
17155
17302
  }
17156
17303
  let cachedToolIds = null;
17157
17304
  let cachedToolIdsAt = 0;
@@ -17488,6 +17635,8 @@ ${reminder}`;
17488
17635
  const w = ensureWatch(sid);
17489
17636
  const messages = await loadMessages(sid);
17490
17637
  noteInboundUserMessage(sid, w, messages);
17638
+ if (await recoverSubagentResponse(sid, w, messages))
17639
+ return;
17491
17640
  if (await recoverSilentDeadStream(sid, messages))
17492
17641
  return;
17493
17642
  if (await checkForStuckToolCalls(sid, w))
@@ -18938,7 +19087,9 @@ ${reminder}`;
18938
19087
  });
18939
19088
 
18940
19089
  // src/dispatch.ts
19090
+ var PLUGIN_VERSION3 = package_default.version;
18941
19091
  var noopDispose = () => {};
19092
+ var isV2ServiceContext = (ctx) => typeof ctx?.plugin?.add === "function" && typeof ctx?.agent?.transform === "function" && typeof ctx?.skill?.transform === "function";
18942
19093
  var dispatcher = {
18943
19094
  id: "auto-resume.v2",
18944
19095
  server: AutoResumePlugin,
@@ -18951,7 +19102,11 @@ var dispatcher = {
18951
19102
  if (ctx?.serverUrl || ctx?.client?.session) {
18952
19103
  return AutoResumePlugin(ctx, ctx.options);
18953
19104
  }
18954
- 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).`);
18955
19110
  return noopDispose;
18956
19111
  }
18957
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.3",
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",