Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
7511aea
feat(plugin): add opencode v2 port with event-driven architecture
valentimarco Aug 25, 2026
5e04ade
feat(plugin): show visible notification when recovering from stall/fa…
valentimarco Aug 26, 2026
ad92b39
feat(v2): target stable @opencode/plugin 2.0.5 API
valentimarco Sep 17, 2026
aa89252
docs(v2): add install guide and stable migration notes
valentimarco Sep 17, 2026
b5fd8aa
docs(v2): document @opencode/plugin resolution requirement
valentimarco Sep 17, 2026
5491286
Merge remote-tracking branch 'upstream/master' into pr20-v2port
famewolf Sep 24, 2026
0a0a961
OOC lock (parity with v1 loop-fix)
Sep 26, 2026
9a44699
v2: never interrupt compaction or live busy sessions
famewolf Sep 27, 2026
0dd0403
fix(v2): never nudge a turn that hands off to the user
famewolf Sep 27, 2026
3c1d88c
v2 back-port: shouldStandDownForUser guard (v1 parity) - stand down o…
famewolf Sep 28, 2026
875d7e4
fix(v2): stop self-inflicted "Step interrupted" and synthetic-continu…
Sep 28, 2026
67c34ff
fix(v2): stand down when a question or permission is awaiting the user
famewolf Sep 29, 2026
4855d18
fix(v2): handle the session.revert.* family so a rewind drops watch s…
famewolf Sep 30, 2026
7135c46
feat(v2): read every v1 option, warn on unknown keys, and complete bu…
famewolf Oct 1, 2026
c871c29
feat(v2): sweep sessions on startup, and give the plugin a log file a…
famewolf Oct 1, 2026
e61dfec
feat(v2): catch the premature stop, and make activeUserWindowMs real
famewolf Oct 1, 2026
f93dfa1
feat(v2): read the token window and route saturated sessions like v1 …
famewolf Oct 1, 2026
d301bd2
feat(v2): catch the stream that finished without saying anything
famewolf Oct 1, 2026
887ae94
feat(v2): ask for the tool call when the model writes one in its reas…
famewolf Oct 1, 2026
16fdc92
feat(v2): read the todo list, and stop trusting the emoji on its own
famewolf Oct 1, 2026
7d7c8c3
feat(v2): offer the model an explicit way to say it is finished
famewolf Oct 1, 2026
516c40f
feat(v2): name a replacement when a model calls a tool that does not …
famewolf Oct 1, 2026
3c15386
feat(v2): recover a parent left waiting on a subagent that will not a…
famewolf Oct 1, 2026
537b2d3
feat(v2): judge a finished turn once its text has settled, not the in…
famewolf Oct 1, 2026
d890f6c
Merge upstream/master into the v2 port
famewolf Oct 1, 2026
263a8e8
docs(v2): the install guide was describing a plugin this is not
famewolf Oct 1, 2026
805aca4
Drop the session.reverted case: v1 never emitted it
famewolf Oct 1, 2026
a83e158
fix(v2): read todos from the session message log, not the dead storag…
famewolf Oct 3, 2026
801e22e
docs: correct why the storage fallback cannot be promoted
Oct 3, 2026
a355db3
docs: say why the todo parser is a copy and not an import
Oct 3, 2026
5f3fb0a
docs: sync todo storage section to namespaced reality, 780 count
famewolf Oct 3, 2026
ef25e39
test: gate back-to-back idles on observed nudge, not 10ms hope
famewolf Oct 3, 2026
f773ea6
docs: recommend opencode-todo-fork as the confirmed todo plugin
famewolf Oct 3, 2026
2052c07
v2: cross-instance duplicate check via session log
famewolf Oct 5, 2026
7fb1b9f
v2: one live instance per process, and a per-session inject mutex
famewolf Oct 5, 2026
4ccbf80
v2: put the singleton on globalThis — module scope cannot see re-eval…
famewolf Oct 5, 2026
8a34aa5
v2: thinking/tool-call turns are alive — dead-stream detector no long…
famewolf Oct 5, 2026
7fdc472
v2: own prompts don't re-arm budgets; bash->shell alias before fuzzy …
famewolf Oct 5, 2026
333a4e1
v2: quota failures stand down on a gate ladder instead of retrying in…
famewolf Oct 5, 2026
0fef903
v2: own-prompt re-arm exclusion survives null-body projections via in…
famewolf Oct 5, 2026
05838f7
v2: re-arm keeps examined parts — stale errors never re-suggest
famewolf Oct 5, 2026
c36c560
v2: skip busy-stall recovery while a parent waits on live subagents
famewolf Oct 5, 2026
a38d681
test: drop the module-scope assertion superseded by the globalThis re…
famewolf Oct 5, 2026
67437d8
v2: visible stall continue + rich prod text + duplicate anti-repeat
famewolf Oct 5, 2026
90113a7
v2: AUTO_RESUME_VISIBLE_CONTINUE env fallback + startup line reports …
famewolf Oct 5, 2026
3df2339
v2: visibleContinue defaults true (config options dropped by parser)
famewolf Oct 5, 2026
0497165
test: stand-down CONTROL follows visible-default channel
famewolf Oct 5, 2026
a9f2425
test: keep test sessions out of the LIVE auto-resume log
famewolf Oct 5, 2026
963a9fd
docs: dead-stream tool-call exception, plugins-vs-plugin delivery
famewolf Oct 5, 2026
e89bd0b
docs: v1 backport research (rich text, dup guard, startup log, silent…
famewolf Oct 5, 2026
27add46
docs: refresh v2 suite figures and feature list in migration.md
famewolf Oct 5, 2026
7cc0714
v2: inert configProbe option — proves plugins[].options delivery with…
famewolf Oct 5, 2026
3c7ce7f
v2: busy-stall stands down while a turn waits on the user
famewolf Oct 5, 2026
43246e6
v2: subagent protection without ctx.client + cancel stall timers on d…
famewolf Oct 6, 2026
0c07d32
v2: verify delivery before fallback sends (no twin prods)
famewolf Oct 6, 2026
5f85adb
v2: quiet subagents wait inside a patience window; orphan aborts term…
famewolf Oct 6, 2026
511d64d
test: parent-wait CONTROL drops the child link entirely
famewolf Oct 6, 2026
22d8f28
v2: dead instances send nothing (liveness checks on every send path)
famewolf Oct 6, 2026
4eeaed8
v2: genuine user message re-arms after user interrupt (timestamp-gated)
famewolf Oct 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ node_modules/
dist/
.DS_Store
*.log

# v2 build output (bun build src/v2/index.ts --outdir dist-v2 --target bun)
dist-v2/
126 changes: 115 additions & 11 deletions README.md

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

478 changes: 478 additions & 0 deletions docs/known-issues-v2.md

Large diffs are not rendered by default.

223 changes: 223 additions & 0 deletions docs/v2/backport-to-v1.md

Large diffs are not rendered by default.

162 changes: 162 additions & 0 deletions docs/v2/installing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Installing opencode-auto-resume on OpenCode v2

This guide installs the **v2 port** of the plugin. It targets the stable v2
plugin API (`@opencode/plugin` 2.0.5, shipped with `opencode` v2.0.5).

> Coming from OpenCode v1? Read [`migration.md`](./migration.md) first — the v1
> plugin file does **not** run on v2 and must be replaced by the v2 port.

## Requirements

- **opencode v2.0.5 or newer** (`opencode --version`).
- **No runtime dependencies.** `src/v2/index.ts` imports only `node:fs`,
`node:os` and `node:path`, and defines its own `{ id, setup }` helper rather
than importing one, so the built file is self-contained. `@opencode/plugin` is
not needed to *run* the plugin — only to typecheck the source against the
official types (see [Development](#development)).
- **Host access beyond the plugin context.** Reading the todo list needs the
local server's address, and that is not exposed as an environment variable under
OpenChamber. The plugin reads `/proc/self/cmdline` for the `--port` flag (Linux)
and may issue a loopback `GET` to `127.0.0.1`; where `/proc` is unavailable it
falls back to `OPENCODE_SERVER_URL` / `OPENCODE_SERVER_PORT` /
`OPENCODE_PORT` / `PORT`. If `OPENCODE_SERVER_PASSWORD` (or `OPENCODE_PASSWORD`)
is set, it is sent as HTTP Basic on that request. All of it is best-effort: a
host that refuses any of it degrades to "cannot read todos", never a crash.
- The plugin source: [`src/v2/index.ts`](../../src/v2/index.ts) from this repo, or
the built `dist/v2/index.js`.

## Install

There is nothing to install first. Copy the file, and opencode loads it.

### Step 1 — add the plugin

#### Option A — drop-in file (no config)

OpenCode auto-loads every plugin found in these directories:

- Global: `~/.config/opencode/plugins/`
- Per project: `<project>/.opencode/plugins/`

Copy the v2 port there:

```sh
mkdir -p ~/.config/opencode/plugins
cp src/v2/index.ts ~/.config/opencode/plugins/auto-resume-v2.ts
```

That's it — no `opencode.json` change is required. Restart opencode (or reload
plugins) and the plugin is active.

#### Option B — `opencode.json(c)` entry

Use this when you want to load the file from another location, pass options, or
pin a published package. Add an entry to the `plugins` array:

```jsonc title="~/.config/opencode/opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"plugins": [
// 1. plain path (relative to the config file) or absolute path
"./plugins/auto-resume-v2.ts",

// 2. with options
{
"package": "./plugins/auto-resume-v2.ts",
"options": {
"chunkTimeoutMs": 180000,
"maxRetries": 3
}
}
]
}
```

Both `.opencode/plugin/` (v1 directory name) and `.opencode/plugins/` are
discovered; use `.opencode/plugins/` for v2 files.

### Step 2 — restart opencode

Restart opencode after copying the file. A hot plugin reload is **not**
reliable on every version, so restart when in doubt — a missing banner is the
symptom to check for.

## Options

Every option is optional and is read from `ctx.options` in `setup`. The port
reads **all** of v1's options, and applies all of them — there are none left in an
"accepted but not applied" state. The full table with every default is in the
[README](../../README.md#configurable-options); the ones worth knowing about on
v2 specifically:

| Option | Default | Description |
|---|---|---|
| `chunkTimeoutMs` | `180000` | Silence on a **busy** session before recovery is considered. Matches v1 since `49957b2`. |
| `toolTextCheckDelayMs` | `3000` | Settle delay before a finished turn's closing text is judged against the done/tool patterns. Dead streams are still caught on the idle event, because a stream that died before delivering any text has nothing for those patterns to read. `0` judges on the idle event. |
| `logFile` | `~/.local/state/opencode-v2/auto-resume.log` | Where this build logs. **v2 removed v1's server log endpoint**, so without this the plugin has no log at all; `AUTO_RESUME_LOG_FILE` in the environment overrides it. Size-capped at 2 MB. |
| `injectIntervalMs` | `15000` | Minimum gap between recovery injections for one session. No v1 equivalent. |
| `subagentNativeCompactionEnabled` | `false` | Opt-in native `session.compact()` for a saturated subagent, instead of leaving it alone. |
| `debug` | `false` | Verbose `[auto-resume:debug]` logging, appended to `logFile`. |

Example:

```jsonc
{
"plugins": [
{
"package": "./plugins/auto-resume-v2.ts",
"options": { "chunkTimeoutMs": 60000, "debug": true }
}
]
}
```

## Verify it loaded

Start opencode; the plugin logs its banner at load, into `logFile`:

```
[auto-resume] ready (opencode v2). timeout=180000ms interval=5000ms retries=3 loop=3/600s warmup=15000ms stall=continue
```

To see an intervention in action, let a session go quiet past
`chunkTimeoutMs`; the plugin injects a visible **synthetic** message in the
session timeline (`auto-resume: …`) and resumes the turn. Every recovery is
appended to `logFile` with an `[auto-resume]` prefix — the opencode log no longer
has a plugin sink to write to, which is why `logFile` exists.

## Disable / uninstall

- **Disable by id** without touching other plugins: add `"-auto-resume.v2"` to
the `plugins` array.
- **Temporarily off**: delete/move the file out of the `plugins/` directory.
- **Uninstall**: remove the file and any `plugins` entry you added.

## Troubleshooting

| Symptom | Check |
|---|---|
| No `[auto-resume] ready …` banner | File is in a `plugins/` dir opencode scans, or listed in `plugins`; restart opencode. |
| Logs say `failed to load plugin` with no reason | The file is not a valid ES module, or a syntax error. Run `bunx tsc --noEmit` on it (see [Development](#development)). |
| Nothing happens on a stall | Increase verbosity with `"debug": true`; confirm `chunkTimeoutMs` isn't larger than your real stall. |
| Recovers but you don't see a notice | Your model/provider may reject `session.synthetic()`; the plugin falls back to `session.prompt()` (no TUI banner). |
| Never recovers a parent waiting on a subagent | Intentional: parent sessions blocked on a running subagent are left alone. |
| Never recovers while a permission dialog is open | Intentional: recovery is held until the permission is answered. |

## Development

The source has no imports from `@opencode/plugin` — the `{ id, setup }` helper
is defined locally — so the package is only needed to check the file against the
official types:

```sh
bun add -d @opencode/plugin@2.0.5 typescript
bunx tsc --noEmit --strict --target ESNext --module ESNext \
--moduleResolution bundler --skipLibCheck src/v2/index.ts

# the distributable is a single self-contained file
bun build src/v2/index.ts --outfile dist/v2/index.js --target bun
```

See [`migration.md`](./migration.md) for the full v1→v2 mapping and the
stable-vs-beta validation notes.
Loading
Loading