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 +8 -2
- package/dist/index.js +160 -5
- 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,
|
|
@@ -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",
|
|
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
|
-
|
|
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",
|
|
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",
|