opencode-auto-resume 1.1.10 → 1.1.12
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 +71 -9
- package/dist/index.js +742 -671
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ LLM sessions fail in predictable ways. This plugin monitors all sessions and aut
|
|
|
12
12
|
|
|
13
13
|
The stream goes silent but the session stays "busy". The UI shows a blinking cursor with no progress. If no events arrive for 48 seconds (`chunkTimeoutMs` + `gracePeriodMs`), the plugin sends `"continue"` with exponential backoff. After 3 failed attempts it gives up.
|
|
14
14
|
|
|
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
|
+
|
|
15
17
|
The plugin extracts the **agent, model, and provider** from the last session message, so it resumes with the exact same configuration the user was using (build, sisyphus, prometheus, etc.).
|
|
16
18
|
|
|
17
19
|
_Motivated by:_
|
|
@@ -40,7 +42,7 @@ _Motivated by:_
|
|
|
40
42
|
|
|
41
43
|
The model generates the same broken output repeatedly. Each `continue` just picks up the broken generation. If a session needs 3+ continues within 10 minutes, the plugin aborts the request and sends `"continue"` fresh, forcing a clean restart.
|
|
42
44
|
|
|
43
|
-
A separate **tool-call loop detector** also catches the model calling the same tool 3+ consecutive times (or repeating patterns of length 2-5 occurring at least three times). When detected, it emits `TOOL_LOOP_RECOVERY_PROMPT` to break the loop instead of blindly continuing.
|
|
45
|
+
A separate **tool-call loop detector** also catches the model calling the same tool 3+ consecutive times (or repeating patterns of length 2-5 occurring at least three times). When detected, it emits `TOOL_LOOP_RECOVERY_PROMPT` (at most twice per session) to break the loop instead of blindly continuing.
|
|
44
46
|
|
|
45
47
|
_Motivated by:_
|
|
46
48
|
- [#22142](https://github.com/anomalyco/opencode/issues/22142) — Repetitive tool-call loops with alibaba-coding-plan-cn/qwen3.6-plus
|
|
@@ -53,7 +55,7 @@ _Motivated by:_
|
|
|
53
55
|
|
|
54
56
|
### Orphan parent
|
|
55
57
|
|
|
56
|
-
A subagent finishes but the parent session stays stuck as "busy" forever. The plugin detects when `busyCount` drops from >1 to 1, waits
|
|
58
|
+
A subagent finishes but the parent session stays stuck as "busy" forever. The plugin detects when `busyCount` drops from >1 to 1, waits `subagentWaitMs` + `gracePeriodMs` (18s default), probes the subagent (recovering a crashed child first if possible), then aborts and resumes the parent.
|
|
57
59
|
|
|
58
60
|
_Motivated by:_
|
|
59
61
|
- [#35066](https://github.com/anomalyco/opencode/issues/35066) — notify parent when subagent sessions finish
|
|
@@ -88,7 +90,7 @@ The AI provider's streaming response can fail mid-stream (connection reset, time
|
|
|
88
90
|
1. **Detection**: `session.error` is classified via `isStreamingFailure()` against the configured error names and message patterns
|
|
89
91
|
2. **State transition**: the session's `pendingRecovery` flag is armed with the error name (`pendingRecoveryReason`) and timestamp (`pendingRecoveryAt`)
|
|
90
92
|
3. **Recovery attempt**: once the session is idle and the backoff delay has elapsed, the timer loop sends a recovery prompt
|
|
91
|
-
4. **Watchdog**: if the session is still not busy
|
|
93
|
+
4. **Watchdog**: if the session is still not busy `toolTextCheckDelayMs` (3s default) after the prompt, the recovery is retried (up to `maxRecoveryRetries`) with exponential backoff
|
|
92
94
|
5. **Escalation**: when retries are exhausted, the plugin aborts the session and resumes it (`abort+resume`); `gaveUp` is set if that also fails
|
|
93
95
|
|
|
94
96
|
#### Configuration
|
|
@@ -128,8 +130,8 @@ See [Recovery Flow Documentation](docs/architecture/recovery-flow.md) for the fu
|
|
|
128
130
|
5. Session goes idle; timer loop waits until the backoff delay has elapsed
|
|
129
131
|
6. Recovery prompt sent (recoveryAttempts=1)
|
|
130
132
|
7. Success → session busy → recovery flags cleared
|
|
131
|
-
Still not busy after 3s → watchdog retry (attempt 2/2)
|
|
132
|
-
Still not busy
|
|
133
|
+
Still not busy after `toolTextCheckDelayMs` (3s default) → watchdog retry (attempt 2/2)
|
|
134
|
+
Still not busy → maxRecoveryRetries reached → abort + resume
|
|
133
135
|
Abort+continue fails → gaveUp
|
|
134
136
|
```
|
|
135
137
|
|
|
@@ -148,9 +150,15 @@ _Motivated by:_
|
|
|
148
150
|
|
|
149
151
|
---
|
|
150
152
|
|
|
153
|
+
### Silent dead-stream recovery
|
|
154
|
+
|
|
155
|
+
The model stream can die after emitting only reasoning — no text part, no tool call — finalizing with `finish: "unknown"`. OpenCode treats the message as completed and the session goes idle, so no error or stall path triggers. On idle, if the last assistant message has a non-standard finish reason, zero text parts, and at least `silentDeadStreamMinTokens` output tokens, the plugin sends a recovery prompt.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
151
159
|
### Active-tool safety guard
|
|
152
160
|
|
|
153
|
-
Before **any** abort, the
|
|
161
|
+
Before **any** abort, two guards run in order: the primary deterministic in-flight counter maintained by the `tool.execute.before`/`tool.execute.after` hooks (`hasInflightTools()`), then a polled `checkSessionHasActiveTool()` fallback for sessions discovered without hooks. If a tool is running, the abort is skipped. This prevents the plugin from killing a long-running build, test suite, or command — even when it looks like a stall.
|
|
154
162
|
|
|
155
163
|
_Motivated by:_
|
|
156
164
|
- [#26063](https://github.com/anomalyco/opencode/issues/26063) — Tool execution aborted/terminated
|
|
@@ -174,6 +182,8 @@ _Motivated by:_
|
|
|
174
182
|
|
|
175
183
|
User presses ESC to cancel a request. The plugin detects `MessageAbortedError` and marks all busy sessions as cancelled, never resuming them. The grace period (`gracePeriodMs`) also lets late ESC/status events arrive before any action.
|
|
176
184
|
|
|
185
|
+
The back-off lifts as soon as the user sends a new prompt in that session (`chat.message` hook): a fresh user message starts a new round of work, so auto-resume re-arms. The plugin's own recovery prompts do not re-arm it. The same applies to the `task_complete` latch.
|
|
186
|
+
|
|
177
187
|
_Motivated by:_
|
|
178
188
|
- [#28453](https://github.com/anomalyco/opencode/issues/28453) — ACP session/cancel emits agent_error for MessageAbortedError before cancelled result
|
|
179
189
|
- [#32432](https://github.com/anomalyco/opencode/issues/32432) — Cancelled subagents can't be opened in TUI + Ctrl+X intermittently fails
|
|
@@ -185,6 +195,8 @@ _Motivated by:_
|
|
|
185
195
|
|
|
186
196
|
The agent can call the built-in `task_complete` tool to signal that all work is done. When invoked, the plugin stops sending any further `"continue"` prompts, clears all pending timers, and marks the session as complete. This replaces fragile text-based heuristics (emoji patterns, language detection) with a deterministic signal.
|
|
187
197
|
|
|
198
|
+
If `task_complete` is called while open todos remain, the call is rejected (up to `maxRetries` times) with a message asking the agent to finish the remaining work first.
|
|
199
|
+
|
|
188
200
|
---
|
|
189
201
|
|
|
190
202
|
### 🎉 emoji completion
|
|
@@ -199,9 +211,15 @@ When the assistant prints phrases like "Ready to continue with task" or "Proceed
|
|
|
199
211
|
|
|
200
212
|
---
|
|
201
213
|
|
|
214
|
+
### Action-intent nudge
|
|
215
|
+
|
|
216
|
+
When the assistant ends a line with `:` ("Next, I will edit the file:") — announcing intent without acting — the plugin sends `actionIntentPrompt` after a short delay, nudging the model to execute. Disable with `resumeOnActionIntent: false`. Detection is skipped while a session is younger than `warmupMs`.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
202
220
|
### Done-claim verification
|
|
203
221
|
|
|
204
|
-
If the assistant claims the task is done ("task done", "finished", "all complete") but open todos remain, the plugin sends `DONE_WITHOUT_WORK_PROMPT` asking the agent to verify and finish remaining work.
|
|
222
|
+
If the assistant claims the task is done ("task done", "finished", "all complete") but open todos remain, the plugin sends `DONE_WITHOUT_WORK_PROMPT` asking the agent to verify and finish remaining work. If it claims done with **no** open todos but the response contains no description of the work, the plugin sends `DONE_WITHOUT_DETAILS_PROMPT` — an imperative prompt demanding a concrete report (files changed, commands run, results). Both use the real `todo.updated` event state — not regex on the message text.
|
|
205
223
|
|
|
206
224
|
---
|
|
207
225
|
|
|
@@ -233,6 +251,32 @@ _Motivated by:_
|
|
|
233
251
|
|
|
234
252
|
---
|
|
235
253
|
|
|
254
|
+
## Recovery model
|
|
255
|
+
|
|
256
|
+
All recovery paths fall into three families. Which family fires determines what the prompt can actually do.
|
|
257
|
+
|
|
258
|
+
### 1. Idle-boundary nudges (safe)
|
|
259
|
+
|
|
260
|
+
Fire only after OpenCode reports the session **idle** — the runner has exited. The prompt starts a new run.
|
|
261
|
+
|
|
262
|
+
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.
|
|
263
|
+
|
|
264
|
+
### 2. Busy-silence continue (stream stall)
|
|
265
|
+
|
|
266
|
+
Fires when the session still reports **busy** but no events arrived for `chunkTimeoutMs` + `gracePeriodMs`. Controlled by `busyStallStrategy`:
|
|
267
|
+
|
|
268
|
+
- `"continue"` (default) — sends a prompt while busy. This only helps when the busy status is **stale** (the runner already exited but the status was never updated); the prompt then starts a new run.
|
|
269
|
+
- `"abort"` — aborts first (`session.abort()`), then sends the prompt. Required to unblock a runner that is genuinely stuck (hung provider stream). Subject to the same active-tool guards as every abort: never fires while a tool is in-flight.
|
|
270
|
+
- `"off"` — disables busy-silence recovery entirely; stalls are then handled only by idle-boundary recovery (if the session ever goes idle) or manually.
|
|
271
|
+
|
|
272
|
+
**Why `"continue"` cannot unblock a live runner:** in OpenCode ≥ 1.18, `session.prompt()` while the runner is `Running` joins the existing run — the message is admitted to the session inbox and promoted at the next provider-turn boundary. A hung stream never reaches that boundary, so the parked message cannot unblock it; if the run later exits, the message may surface as an unwanted extra turn (token cost, race risk). True transport stalls need abort-first.
|
|
273
|
+
|
|
274
|
+
### 3. Abort-first recovery
|
|
275
|
+
|
|
276
|
+
Aborts the active run, then continues. The only family that can unblock a live runner.
|
|
277
|
+
|
|
278
|
+
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).
|
|
279
|
+
|
|
236
280
|
## Architecture
|
|
237
281
|
|
|
238
282
|
```
|
|
@@ -242,6 +286,8 @@ Any SSE Event
|
|
|
242
286
|
|
|
243
287
|
session.status events:
|
|
244
288
|
├─ busy → reset timer, clear retry counters
|
|
289
|
+
├─ retry → touch session
|
|
290
|
+
├─ interrupted → user cancel: back off until the next user message
|
|
245
291
|
└─ idle → schedule tool-text check (3s delay)
|
|
246
292
|
└─ fetch messages → scan for XML / thinking-tool patterns
|
|
247
293
|
├─ found → send recovery prompt (with backoff)
|
|
@@ -257,12 +303,16 @@ Timer loop (every 5s):
|
|
|
257
303
|
├─ orphan watch active? → wait or abort+continue
|
|
258
304
|
├─ busyCount > 1? → skip (subagent running)
|
|
259
305
|
├─ active tool running? → skip (never abort tools)
|
|
260
|
-
└─ idle > 48s? →
|
|
306
|
+
└─ idle > 48s? → busyStallStrategy:
|
|
307
|
+
off → skip · abort → abort+continue
|
|
308
|
+
else → hallucination loop? abort : continue with backoff
|
|
261
309
|
|
|
262
310
|
Periodic (every 60s): session.list() to discover missed sessions
|
|
263
311
|
Periodic: cleanup idle sessions older than 10min or >50 entries
|
|
264
312
|
```
|
|
265
313
|
|
|
314
|
+
Events consumed: `session.status`, `session.created`, `session.updated`, `session.idle`, `session.interrupted`, `session.error`, `todo.updated`, `command.executed`; hooks: `chat.message`, `tool.execute.before`/`after`, `command.execute.before`.
|
|
315
|
+
|
|
266
316
|
## Installation
|
|
267
317
|
|
|
268
318
|
### Opencode
|
|
@@ -319,6 +369,19 @@ With options:
|
|
|
319
369
|
| `streamingFailureErrorNames` | `["ProviderError","APIError","StreamError","ConnectionError","TimeoutError"]` | Error names that classify as streaming failures (exact match) |
|
|
320
370
|
| `streamingFailureMessagePatterns` | `["streaming response failed","stream.*fail","connection.*reset","connection.*closed"]` | Regex patterns (case-insensitive) in error messages indicating streaming failure |
|
|
321
371
|
| `maxRecoveryRetries` | `2` | Max streaming-failure recovery attempts before abort+resume escalation |
|
|
372
|
+
| `toolTextCheckDelayMs` | `3000` | Delay before scanning an idle session for tool-as-text; also the recovery watchdog delay |
|
|
373
|
+
| `minActivityGapMs` | `1000` | Skip recovery if the session was active within this gap |
|
|
374
|
+
| `warmupMs` | `15000` | Action-intent detection disabled while a session is younger than this |
|
|
375
|
+
| `debug` | `false` | Enable `[debug]` console diagnostics |
|
|
376
|
+
| `resumeOnActionIntent` | `true` | Enable action-intent (`:`-terminated line) nudges |
|
|
377
|
+
| `continuePrompt` | `"continue"` | Prompt text for stall/streaming/dead-stream recovery |
|
|
378
|
+
| `actionIntentPrompt` | same as `continuePrompt` | Prompt sent on action-intent detection |
|
|
379
|
+
| `toolTextRecoveryPrompt` | `TOOL_TEXT_RECOVERY_PROMPT` | Override the tool-call-as-text recovery prompt |
|
|
380
|
+
| `thinkingToolRecoveryPrompt` | `THINKING_TOOL_RECOVERY_PROMPT` | Override the thinking-tool recovery prompt |
|
|
381
|
+
| `doneWithoutWorkPrompt` | `DONE_WITHOUT_WORK_PROMPT` | Override the done-claim-with-open-todos prompt |
|
|
382
|
+
| `doneWithoutDetailsPrompt` | `DONE_WITHOUT_DETAILS_PROMPT` | Override the done-claim-with-no-todos report prompt |
|
|
383
|
+
| `silentDeadStreamMinTokens` | `200` | Min output tokens to treat a textless `finish:"unknown"` message as a dead stream |
|
|
384
|
+
| `busyStallStrategy` | `"continue"` | Busy-stall response: `"continue"`, `"abort"` (abort-first), or `"off"` (disabled) |
|
|
322
385
|
|
|
323
386
|
Message patterns are matched case-insensitively. Error names use exact match.
|
|
324
387
|
|
|
@@ -326,7 +389,6 @@ Message patterns are matched case-insensitively. Error names use exact match.
|
|
|
326
389
|
|
|
327
390
|
| Constant | Value | Description |
|
|
328
391
|
|---|---|---|
|
|
329
|
-
| `TOOL_TEXT_CHECK_DELAY_MS` | `3000` | Delay before scanning idle session for tool-as-text |
|
|
330
392
|
| `ABORT_CONTINUE_DELAY_MS` | `2000` | Delay between abort and continue |
|
|
331
393
|
| `MAX_IDLE_SESSIONS` | `50` | Idle session map cap before cleanup |
|
|
332
394
|
| `IDLE_CLEANUP_MS` | `600000` | Idle session age before cleanup (10 min) |
|