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 +8 -2
- package/dist/index.js +172 -8
- package/dist/v2/index.js +98 -1
- package/package.json +4 -3
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(
|
|
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.
|
|
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",
|
|
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
|
-
|
|
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",
|
|
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
|
+
"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
|
-
},
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
32
33
|
"dist/index.js",
|
|
33
34
|
"dist/v2/index.js",
|
|
34
35
|
"README.md",
|