opencode-auto-resume 1.1.19 → 1.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/dist/index.js +43 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ LLM sessions fail in predictable ways. This plugin monitors all sessions and aut
|
|
|
10
10
|
|
|
11
11
|
### Stall recovery
|
|
12
12
|
|
|
13
|
-
The stream goes silent but the session stays "busy". The UI shows a blinking cursor with no progress. If no events arrive for
|
|
13
|
+
The stream goes silent but the session stays "busy". The UI shows a blinking cursor with no progress. If no events arrive for ~3 minutes (`chunkTimeoutMs` + `gracePeriodMs`), the plugin sends `"continue"` with exponential backoff. After 3 failed attempts it gives up.
|
|
14
14
|
|
|
15
15
|
The `busyStallStrategy` option controls this path: `"continue"` (default), `"abort"` (abort-first), or `"off"` — see [Recovery model](#recovery-model) for what a busy prompt can and cannot do while the runner is live.
|
|
16
16
|
|
|
@@ -250,7 +250,7 @@ If the assistant claims the task is done ("task done", "finished", "all complete
|
|
|
250
250
|
The session is not stalled while the ball is in the user's court. Two gates stand down idle nudges:
|
|
251
251
|
|
|
252
252
|
- **Awaiting input**: the newest assistant message holds a `tool_use` part with `state.status: "pending"` (e.g. an open `question` tool call). All idle checks, the periodic recheck, delayed action-intent callbacks, and the tool-text scan skip prompting until a newer user message clears the gate. Completed tool calls never engage it.
|
|
253
|
-
- **Recently active user**: any inbound user message within `activeUserWindowMs` (default
|
|
253
|
+
- **Recently active user**: any inbound user message within `activeUserWindowMs` (default 5 minutes) means the user is engaged — likely composing a reply, which leaves no pending tool call behind. Open-todos nudges (idle + periodic), the tool-text reminder fallback, and action-intent callbacks stand down until the window expires.
|
|
254
254
|
|
|
255
255
|
---
|
|
256
256
|
|
|
@@ -367,7 +367,7 @@ With options:
|
|
|
367
367
|
{
|
|
368
368
|
"plugin": [
|
|
369
369
|
["opencode-auto-resume", {
|
|
370
|
-
"chunkTimeoutMs":
|
|
370
|
+
"chunkTimeoutMs": 180000,
|
|
371
371
|
"gracePeriodMs": 3000,
|
|
372
372
|
"maxRetries": 3
|
|
373
373
|
}]
|
|
@@ -382,7 +382,7 @@ With options:
|
|
|
382
382
|
"plugin": [
|
|
383
383
|
[
|
|
384
384
|
"file:///home/YOURUSER/.config/opencode/plugins/opencode-auto-resume/dist/index.js",
|
|
385
|
-
{ "chunkTimeoutMs":
|
|
385
|
+
{ "chunkTimeoutMs": 180000, "maxRetries": 3 }
|
|
386
386
|
]
|
|
387
387
|
]
|
|
388
388
|
}
|
|
@@ -392,7 +392,7 @@ With options:
|
|
|
392
392
|
|
|
393
393
|
| Option | Default | Description |
|
|
394
394
|
|---|---|---|
|
|
395
|
-
| `chunkTimeoutMs` | `
|
|
395
|
+
| `chunkTimeoutMs` | `180000` | Inactivity timeout before considering stream stalled |
|
|
396
396
|
| `gracePeriodMs` | `3000` | Extra wait before acting (lets ESC/status events arrive) |
|
|
397
397
|
| `checkIntervalMs` | `5000` | Timer poll interval |
|
|
398
398
|
| `maxRetries` | `3` | Max auto-resume attempts before giving up |
|
|
@@ -407,6 +407,7 @@ With options:
|
|
|
407
407
|
| `toolTextCheckDelayMs` | `3000` | Delay before scanning an idle session for tool-as-text; also the recovery watchdog delay |
|
|
408
408
|
| `minActivityGapMs` | `1000` | Skip recovery if the session was active within this gap |
|
|
409
409
|
| `warmupMs` | `15000` | Action-intent detection disabled while a session is younger than this |
|
|
410
|
+
| `discoveryDelayMs` | `5000` | Delay before the initial session discovery run after plugin attach |
|
|
410
411
|
| `debug` | `false` | Enable `[debug]` console diagnostics |
|
|
411
412
|
| `resumeOnActionIntent` | `true` | Enable action-intent (`:`-terminated line) nudges |
|
|
412
413
|
| `continuePrompt` | `"continue"` | Prompt text for stall/streaming/dead-stream recovery |
|
|
@@ -420,7 +421,7 @@ With options:
|
|
|
420
421
|
| `silentDeadStreamMinTokens` | `200` | Min output tokens to treat a textless `finish:"unknown"` message as a dead stream |
|
|
421
422
|
| `busyStallStrategy` | `"continue"` | Busy-stall response: `"continue"`, `"abort"` (abort-first), or `"off"` (disabled) |
|
|
422
423
|
| `contextSaturationThreshold` | `0.85` | Ratio of used/usable context that routes a saturated parent to magic-context `ctx-wrapup` (only when magic-context is installed) |
|
|
423
|
-
| `activeUserWindowMs` | `
|
|
424
|
+
| `activeUserWindowMs` | `300000` | Inbound-user-message recency window (5 min) during which idle nudges stand down (user likely composing) |
|
|
424
425
|
| `subagentNativeCompactionEnabled` | `false` | Opt-in native `session.summarize()` for saturated subagent sessions (no magic-context detection required) |
|
|
425
426
|
|
|
426
427
|
Message patterns are matched case-insensitively. Error names use exact match.
|
|
@@ -445,7 +446,7 @@ The plugin handles all recovery automatically — no manual intervention needed.
|
|
|
445
446
|
| Problem | Solution |
|
|
446
447
|
|---|---|
|
|
447
448
|
| Resumes after ESC | Increase `gracePeriodMs` to `5000` |
|
|
448
|
-
|
|
|
449
|
+
| Stall recovery too aggressive | Decrease `chunkTimeoutMs` (e.g. `45000`) |
|
|
449
450
|
| Too slow to react | Decrease `checkIntervalMs` to `2000` |
|
|
450
451
|
| Orphan parent not detected | Increase `subagentWaitMs` to `20000` |
|
|
451
452
|
| Hallucination loop not caught | Decrease `loopMaxContinues` to `2` |
|
package/dist/index.js
CHANGED
|
@@ -12335,9 +12335,10 @@ function tool(input) {
|
|
|
12335
12335
|
}
|
|
12336
12336
|
tool.schema = exports_external;
|
|
12337
12337
|
// src/index.ts
|
|
12338
|
-
var DEFAULT_CHUNK_TIMEOUT_MS =
|
|
12338
|
+
var DEFAULT_CHUNK_TIMEOUT_MS = 180000;
|
|
12339
12339
|
var DEFAULT_CHECK_INTERVAL_MS = 5000;
|
|
12340
|
-
var
|
|
12340
|
+
var DEFAULT_DISCOVERY_DELAY_MS = 5000;
|
|
12341
|
+
var DEFAULT_ACTIVE_USER_WINDOW_MS = 5 * 60000;
|
|
12341
12342
|
var DEFAULT_GRACE_PERIOD_MS = 3000;
|
|
12342
12343
|
var DEFAULT_MAX_RETRIES = 3;
|
|
12343
12344
|
var DEFAULT_MAX_BACKOFF_MS = 8000;
|
|
@@ -12352,6 +12353,7 @@ var DEFAULT_MIN_ACTIVITY_GAP_MS = 1000;
|
|
|
12352
12353
|
var DEFAULT_WARMUP_MS = 15000;
|
|
12353
12354
|
var DEFAULT_SILENT_DEAD_STREAM_MIN_TOKENS = 200;
|
|
12354
12355
|
var DEFAULT_DEBUG = false;
|
|
12356
|
+
var OOC_ERROR_RE = /exceeds the available context size|context size \(\d+\)|too large to compact|too many tokens|prompt is too long/i;
|
|
12355
12357
|
var DEFAULT_STREAMING_FAILURE_ERROR_NAMES = [
|
|
12356
12358
|
"ProviderError",
|
|
12357
12359
|
"APIError",
|
|
@@ -12659,6 +12661,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12659
12661
|
const maxRecoveryRetries = options?.maxRecoveryRetries ?? DEFAULT_MAX_RECOVERY_RETRIES;
|
|
12660
12662
|
const minActivityGapMs = options?.minActivityGapMs ?? DEFAULT_MIN_ACTIVITY_GAP_MS;
|
|
12661
12663
|
const warmupMs = options?.warmupMs ?? DEFAULT_WARMUP_MS;
|
|
12664
|
+
const discoveryDelayMs = options?.discoveryDelayMs ?? DEFAULT_DISCOVERY_DELAY_MS;
|
|
12662
12665
|
const debug = options?.debug ?? DEFAULT_DEBUG;
|
|
12663
12666
|
const streamingFailureErrorNames = options?.streamingFailureErrorNames ?? DEFAULT_STREAMING_FAILURE_ERROR_NAMES;
|
|
12664
12667
|
const streamingFailureMessagePatterns = options?.streamingFailureMessagePatterns ?? DEFAULT_STREAMING_FAILURE_MESSAGE_PATTERNS;
|
|
@@ -12770,6 +12773,8 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12770
12773
|
resumeAttempts: 0,
|
|
12771
12774
|
lastRetryAt: 0,
|
|
12772
12775
|
gaveUp: false,
|
|
12776
|
+
oocLocked: false,
|
|
12777
|
+
oocLockReason: null,
|
|
12773
12778
|
orphanWatchStartAt: null,
|
|
12774
12779
|
aborting: false,
|
|
12775
12780
|
pluginAbortInFlight: false,
|
|
@@ -12946,6 +12951,10 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
12946
12951
|
await log("debug", `${short(sid)} - gaveUp latched, refusing further continue prompts`);
|
|
12947
12952
|
return;
|
|
12948
12953
|
}
|
|
12954
|
+
if (w.oocLocked) {
|
|
12955
|
+
await log("debug", `${short(sid)} - oocLocked latched, refusing further continue prompts (reason: ${w.oocLockReason ?? "n/a"})`);
|
|
12956
|
+
return;
|
|
12957
|
+
}
|
|
12949
12958
|
if (!w.continuing)
|
|
12950
12959
|
dbg(`State transition on ${short(sid)}: continuing=false -> true`);
|
|
12951
12960
|
w.continuing = true;
|
|
@@ -13831,14 +13840,13 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13831
13840
|
await log("info", `${short(sid)} - max open-todos nudges (${maxRetries}) reached, waiting for activity`);
|
|
13832
13841
|
return;
|
|
13833
13842
|
}
|
|
13834
|
-
w.todoNudgeAttempts++;
|
|
13835
13843
|
} else if (isDoneClaimNoTodos) {
|
|
13836
13844
|
w.doneClaimNoTodosAttempts++;
|
|
13837
13845
|
} else {
|
|
13838
13846
|
w.toolTextRecovered = true;
|
|
13839
13847
|
w.toolTextAttempts++;
|
|
13840
13848
|
}
|
|
13841
|
-
const attemptNum = isOpenTodosReminder ? w.todoNudgeAttempts : isDoneClaimNoTodos ? w.doneClaimNoTodosAttempts : w.toolTextAttempts;
|
|
13849
|
+
const attemptNum = isOpenTodosReminder ? w.todoNudgeAttempts + 1 : isDoneClaimNoTodos ? w.doneClaimNoTodosAttempts : w.toolTextAttempts;
|
|
13842
13850
|
await log("info", `${bestCandidate.source} detected on ${short(sid)}! ` + `Attempt ${attemptNum}/${maxRetries}. Sending recovery prompt...`);
|
|
13843
13851
|
const timeSinceActivity = Date.now() - w.lastActivityAt;
|
|
13844
13852
|
if (timeSinceActivity < minActivityGapMs) {
|
|
@@ -13884,6 +13892,8 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13884
13892
|
return false;
|
|
13885
13893
|
if (w.aborting)
|
|
13886
13894
|
return false;
|
|
13895
|
+
if (w.oocLocked)
|
|
13896
|
+
return false;
|
|
13887
13897
|
const idleSec = Math.round((Date.now() - (w.orphanWatchStartAt ?? w.lastActivityAt)) / 1000);
|
|
13888
13898
|
await log("info", `Abort+Resume on ${short(sid)} (${idleSec}s idle). Aborting...`);
|
|
13889
13899
|
w.pluginAbortInFlight = true;
|
|
@@ -13968,12 +13978,13 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
13968
13978
|
try {
|
|
13969
13979
|
const response = await ctx.client.session.list();
|
|
13970
13980
|
const list = extractMessages(response);
|
|
13981
|
+
const statusMap = await getSessionStatusMap();
|
|
13971
13982
|
for (const s of list) {
|
|
13972
13983
|
const sid = s.id;
|
|
13973
13984
|
if (sid && typeof sid === "string" && sid.startsWith("ses_")) {
|
|
13974
13985
|
const isNew = !sessions.has(sid);
|
|
13975
13986
|
ensureWatch(sid);
|
|
13976
|
-
const status = s.status;
|
|
13987
|
+
const status = s.status ?? statusMap[sid];
|
|
13977
13988
|
if (status) {
|
|
13978
13989
|
const w = sessions.get(sid);
|
|
13979
13990
|
w.status = status;
|
|
@@ -14216,7 +14227,7 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14216
14227
|
}, SESSION_DISCOVERY_INTERVAL_MS);
|
|
14217
14228
|
if (discoveryTimer.unref)
|
|
14218
14229
|
discoveryTimer.unref();
|
|
14219
|
-
setTimeout(discoverSessions,
|
|
14230
|
+
setTimeout(discoverSessions, discoveryDelayMs);
|
|
14220
14231
|
}
|
|
14221
14232
|
startTimer();
|
|
14222
14233
|
async function handleEvent(ev) {
|
|
@@ -14237,7 +14248,11 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14237
14248
|
if (w.pendingRecovery) {
|
|
14238
14249
|
dbg(`Pending recovery cleared on ${short(sid)}: reason=session-busy`);
|
|
14239
14250
|
}
|
|
14240
|
-
|
|
14251
|
+
if (w.continuing) {
|
|
14252
|
+
dbg(`busy on ${short(sid)} via recovery prompt - preserving resumeAttempts=${w.resumeAttempts} gaveUp=${w.gaveUp}`);
|
|
14253
|
+
} else {
|
|
14254
|
+
resetBusyFlags(w);
|
|
14255
|
+
}
|
|
14241
14256
|
prevBusyCount = busyCount();
|
|
14242
14257
|
log("debug", `${short(sid)} -> busy (${prevBusyCount})`);
|
|
14243
14258
|
} else if (statusType === "interrupted") {
|
|
@@ -14379,12 +14394,14 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14379
14394
|
if (isCelebration) {
|
|
14380
14395
|
await log("info", `${short(sid)} - \uD83C\uDF89 detected but ${open.length} open todos remain, sending nudge`);
|
|
14381
14396
|
const reminder = buildOpenTodosReminder(todos);
|
|
14382
|
-
await tryResume(sid, w, "Idle with open todos (celebration false positive)", reminder);
|
|
14383
|
-
|
|
14397
|
+
const sent = await tryResume(sid, w, "Idle with open todos (celebration false positive)", reminder);
|
|
14398
|
+
if (sent)
|
|
14399
|
+
w.todoNudgeAttempts++;
|
|
14384
14400
|
} else {
|
|
14385
14401
|
const reminder = buildOpenTodosReminder(todos);
|
|
14386
|
-
await tryResume(sid, w, "Idle with open todos", reminder);
|
|
14387
|
-
|
|
14402
|
+
const sent = await tryResume(sid, w, "Idle with open todos", reminder);
|
|
14403
|
+
if (sent)
|
|
14404
|
+
w.todoNudgeAttempts++;
|
|
14388
14405
|
}
|
|
14389
14406
|
}
|
|
14390
14407
|
}
|
|
@@ -14626,6 +14643,11 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14626
14643
|
if (w) {
|
|
14627
14644
|
w.pendingTools = 0;
|
|
14628
14645
|
w.pendingCommands = 0;
|
|
14646
|
+
if (!w.oocLocked && OOC_ERROR_RE.test(errorMessage ?? "")) {
|
|
14647
|
+
w.oocLocked = true;
|
|
14648
|
+
w.oocLockReason = (errorMessage ?? "").slice(0, 200);
|
|
14649
|
+
await log("warn", `${short(sid)} - OOC error latched, halting recovery (reason: ${w.oocLockReason})`);
|
|
14650
|
+
}
|
|
14629
14651
|
}
|
|
14630
14652
|
}
|
|
14631
14653
|
break;
|
|
@@ -14638,7 +14660,11 @@ var AutoResumePlugin = async (ctx, options) => {
|
|
|
14638
14660
|
if (w.pendingRecovery) {
|
|
14639
14661
|
dbg(`Pending recovery cleared on ${short(sid)}: reason=user-command`);
|
|
14640
14662
|
}
|
|
14641
|
-
|
|
14663
|
+
if (w.continuing) {
|
|
14664
|
+
dbg(`command on ${short(sid)} via recovery prompt - preserving resumeAttempts=${w.resumeAttempts} gaveUp=${w.gaveUp}`);
|
|
14665
|
+
} else {
|
|
14666
|
+
resetBusyFlags(w);
|
|
14667
|
+
}
|
|
14642
14668
|
w.pendingCommands = Math.max(0, w.pendingCommands - 1);
|
|
14643
14669
|
w.lastActivityAt = Date.now();
|
|
14644
14670
|
}
|
|
@@ -14726,6 +14752,11 @@ ${reminder}`;
|
|
|
14726
14752
|
w.lastActivityAt = Date.now();
|
|
14727
14753
|
if (w.continuing)
|
|
14728
14754
|
return;
|
|
14755
|
+
if (w.oocLocked) {
|
|
14756
|
+
w.oocLocked = false;
|
|
14757
|
+
w.oocLockReason = null;
|
|
14758
|
+
await log("info", `${short(sid)} - genuine user message, clearing oocLocked`);
|
|
14759
|
+
}
|
|
14729
14760
|
if (w.userCancelled || w.completionSignaled) {
|
|
14730
14761
|
w.userCancelled = false;
|
|
14731
14762
|
w.completionSignaled = false;
|
package/package.json
CHANGED