opencode-auto-nudge
v1.0.1
Published
Keeps OpenCode agent sessions alive: auto-retries rate-limited, timed-out, overloaded or empty model turns with capped backoff, then gives up loudly instead of silently stalling. Plays distinct sounds for task completed, failure, and human-decision-needed
Maintainers
Readme
opencode-auto-nudge
An OpenCode plugin that keeps agent sessions alive: it retries failed or empty model turns automatically, gives up loudly instead of silently, and plays distinct sounds when your attention is actually needed — so you stop typing continue every few minutes.
The problem
During long agentic runs, OpenCode sessions can stall in ways that are easy to miss:
- Provider hiccups (429 rate limits, 5xx, timeouts) exhaust OpenCode's built-in retries and the turn just ends.
- Empty responses — the model returns a turn with zero content (only internal step markers), the loop exits, and nothing tells you.
- Permission prompts sit unanswered while you're in another window.
opencode-auto-nudge watches session events and closes each of these gaps: transient failures get retried with backoff, empty turns get a "continue exactly where you left off" nudge, and every terminal outcome is surfaced via toast, log line, and sound.
What it does
| Situation | Behavior |
|---|---|
| Retryable provider error (429, 5xx, timeout, overloaded, network…) | Re-prompts the same session after exponential backoff (3s → 30s), up to MAX_ATTEMPTS |
| Turn completed but produced no output | Watchdog detects it on session.idle and injects a continuation prompt within seconds |
| Retries exhausted | Explicit "giving up" warn log + error toast + error sound. Never silently stops |
| Permission / question waiting for you | Attention ping, repeated on an interval until you answer |
| Task finished legitimately | Short ascending chime (long turns only) |
| You aborted (esc) or typed a message | Nothing is auto-continued; counters reset |
Sound signals
Sounds are synthesized at startup into ~/.cache/opencode/sounds/ — no asset files needed and no dependency on any specific sound driver or device.
Playback is a fallback chain, not a single player: every detected player among paplay (PulseAudio), pw-play (PipeWire), ffplay, afplay (macOS), aplay (ALSA) is tried in order until one exits successfully. Player detection uses Bun's native which inside OpenCode (no shell quirks) with a portable fallback elsewhere. If no player exists at all, the plugin degrades silently to toasts/logs — or, with AUTO_SOUND_BEL=1, emits a terminal bell (\a) as a last resort (terminal-dependent; many terminals mute it by default).
Each turn-end writes a breadcrumb to ~/.cache/opencode/sounds/debug.json recording the classification decision (success, skip-short-turn, nudged, …) plus the last playback attempt result — so "why no sound?" is always answerable from one file.
| File | Pattern | Meaning |
|---|---|---|
| success.wav | two ascending tones (880→1318 Hz) | turn completed with real output |
| error.wav | descending low buzz (330→196 Hz) | turn died in a non-abort error, or retries were exhausted |
| attention.wav | urgent double high ping (1568 Hz) | permission/question needs your decision; re-pings every AUTO_SOUND_REMIND_MS up to AUTO_SOUND_REMIND_MAX times |
Deliberate silences so this doesn't get annoying:
- Turns shorter than
AUTO_SOUND_SHORT_TURN_MS(you're actively chatting anyway) - Aborts (
esc) — you did that on purpose - Subagent (child) sessions — no false "done!" while the parent keeps working
- Intermediate retry attempts — only outcomes beep
- Headless environments without an audio player — falls back to toasts/logs only
Installation
Requires OpenCode. Restart OpenCode after any install method — plugins load once at startup.
Option A — npm package (recommended):
// ~/.config/opencode/opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-auto-nudge"]
}No sound files to download or install — the three tones are synthesized programmatically on your machine at every startup (pure JS, ~47 KB of PCM). Nothing ships as binary assets, nothing depends on a particular sound driver.
Option B — global drop-in (no npm):
curl -o ~/.config/opencode/plugins/auto-nudge.ts \
https://raw.githubusercontent.com/ay8-dev/opencode-auto-nudge/main/index.tsOption C — per-project drop-in:
cp index.ts .opencode/plugins/auto-nudge.tsConfiguration
All knobs are environment variables (set them where you launch opencode). Defaults are tuned for unattended runs; everything works out of the box.
Retry ladder
| Variable | Default | Description |
|---|---|---|
| AUTO_NUDGE_MAX_ATTEMPTS | 5 | Max nudges per failure episode |
| AUTO_NUDGE_EMPTY_DELAY_MS | 1500 | Delay before re-prompting an empty response |
| AUTO_NUDGE_ERROR_BASE_MS | 3000 | Base delay for error backoff (doubles per attempt) |
| AUTO_NUDGE_ERROR_MAX_MS | 30000 | Backoff ceiling |
| AUTO_NUDGE_MIN_GAP_MS | 1200 | Collapse duplicate triggers for the same failure |
| AUTO_NUDGE_BUSY_RECHECK_MS | 2000 | Wait before re-checking if the session was busy at fire time |
| AUTO_NUDGE_BUSY_RECHECK_MAX | 10 | Max busy-rechecks before dropping a retry (logged loudly) |
| AUTO_NUDGE_TTL_MS | 300000 | Failure counter expires after this much quiet time |
Sounds
| Variable | Default | Description |
|---|---|---|
| AUTO_SOUND_DISABLE | unset (1 disables) | Turn all sounds off; toasts/logs remain |
| AUTO_SOUND_DIR | ~/.cache/opencode/sounds | Where generated wavs live |
| AUTO_SOUND_SHORT_TURN_MS | 4000 | Turns faster than this don't play success (interactive chat) |
| AUTO_SOUND_REMIND_MS | 20000 | Interval for permission reminders |
| AUTO_SOUND_REMIND_MAX | 2 | Extra reminder pings per permission ask |
| AUTO_SOUND_GAIN | 100 | Playback gain in percent (20–400) |
| AUTO_SOUND_BEL | 0 | Emit terminal bell as last-resort fallback when no player works |
Safety design
This plugin is built to never make things worse:
- Hard caps everywhere — attempt caps, TTL expiry, min-gap deduplication, and bounded rescheduling mean no runaway loops, even if a provider returns instant failures forever.
- Aborts are sacred — turns ended by user abort are never auto-continued.
- User intervention wins — typing any message resets the failure counters.
- Failed prompt calls refund their attempt — a rejected nudge doesn't burn budget.
- Errors attached to a turn (auth failures etc.) are not retried — nudging can't fix a bad API key.
- Every action is logged under the
[auto-nudge]tag with service nameauto-nudge.
Observability
grep "\[auto-nudge\]" ~/.local/share/opencode/log/opencode.logExpect lines like:
[auto-nudge] session ses_xxx retry 2/5 in 3000ms (empty response)
[auto-nudge] session ses_xxx recovered after 2 nudge(s)
[auto-nudge] giving up on session ses_xxx after 5 attempts (empty response)
[auto-nudge] permission ask detected (per_xxx)
[auto-nudge] sounds on via pw-playToasts appear in the TUI: warning variants for retries, error variant on give-up.
Known limitations
- Same-model retries only. If a provider is down for good, the ladder burns its attempts and reports loudly. It intentionally does not switch models — pair it with a model-failover plugin if you want that layer (mind the overlap: both react to the same events).
- Heuristic empty detection. A turn counts as empty when the last assistant message has no text/tool/reasoning parts. Exotic part types could be misclassified.
- Coupled to OpenCode event shapes —
session.error,session.idle,session.status,message.updated,permission.asked/replied,question.asked/replied/rejected— verified against OpenCode source and live logs (Aug 2026). Both legacy bus names and internal.v2.schema names are handled for permissions and questions. Note: the plugin-facing event bus delivers the non-prefixed names;permission.v2.*/question.v2.*exist only in core internals. Verified against OpenCode 1.18.x; future releases may change shapes and require plugin updates. Theinit.jsonmarker written intoAUTO_SOUND_DIRat startup ({"player": "...", "at": "..."}) doubles as a load-health check: no file = plugin didn't initialize,"player": null= no audio player found. - Permission reminder depends on
permission.asked/permission.repliedevents matching expected id fields; if those change, pings may not repeat (the initial ping comes from the same handler). - Subagent sessions are nudged but silent. A parent that already given up on a child can see up to
MAX_ATTEMPTSlate child-resurrections — usually desirable, occasionally surprising. - Tested on Linux (Wayland/PipeWire) with Node/Bun. macOS/Windows player probing includes
afplay/ffplaypaths but is not battle-tested.
Troubleshooting
| Symptom | Check |
|---|---|
| No sounds at all | Read ~/.cache/opencode/sounds/init.json: "players": [] means no player binary was found on PATH. Test manually: pw-play ~/.cache/opencode/sounds/success.wav |
| Chime missing on some turns | Read debug.json: the last turn's action field explains the decision (skip-short-turn = under threshold, skip-child = subagent, etc.) |
| No sound on a permission prompt but logs show permission ask detected | Audio server issue, not the plugin |
| Nudges seem absent | Is there any [auto-nudge] line at all? None = plugin didn't load — restart OpenCode |
| Gives up too early/late | Tune AUTO_NUDGE_MAX_ATTEMPTS and backoff vars |
| Want silence | AUTO_SOUND_DISABLE=1 |
Development
npm install
npm test # builds dist/ then runs the offline test suite (no OpenCode instance needed)The test suite mocks the OpenCode SDK client and shell, so it verifies decision logic end-to-end — retry chains, dedup, caps, re-arm, sound routing — without touching a running instance.
