Skip to content

Add OpenCode v2 port (src/v2) + OOC lock parity - #37

Open
famewolf wants to merge 53 commits into
Mte90:masterfrom
famewolf:pr20-v2port
Open

famewolf wants to merge 53 commits into
Mte90:masterfrom
famewolf:pr20-v2port

Conversation

@famewolf

@famewolf famewolf commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

[PR] OpenCode v2 port (src/v2/index.ts) on the stable @opencode/plugin API

Full disclosure — this is a port, and it was done with the assistance of AI.

src/v2/index.ts is a port of v1's behavior, not new functionality. Every recovery path it contains already exists in src/index.ts; the work was re-expressing that behavior against v2's plugin API, and keeping the two in step. The same holds for the test suite: its 188 tests exist to prove the port matches v1, not to specify anything new.

The implementation, the tests and these docs were produced with the assistance of an AI coding agent (via OpenCode) working from the v1 source, and were reviewed by the author. Flagging it plainly because a reviewer is entitled to know how the code was produced before reading it. A port this size is exactly the kind of change where "the tests pass" is weaker evidence than it looks: the tests and the code came out of the same session. Every claim in this description is checkable against the diff, and the v1 module is untouched precisely so the comparison is easy to make by hand.

One defect in v1 fell out of the port work and is not addressed here: a revert does not clear watch state, so a rewind carries one turn's watch state into the next. This PR does not touch v1, and the revert is written up separately against master with a patch. A related v1 bug — an idle nudge firing while a question awaited input — was found earlier and is #27, fixed on master in 7796b2f. The same stand-down guard is in the v2 port.

A second plugin module for the same recovery behavior, written against the stable v2 API. src/index.ts (v1) is not touched: the PR's diff is 21 files, none of them v1.

Branch pr20-v2port (famewolf/opencode-auto-resume)
Port src/v2/index.ts, 3,514 lines
Options all 31 v1 options read and applied, plus 2 v2-only
Tests 13 v2 test files, 188 tests; 780 pass / 0 fail / 48 files repo-wide
Docs docs/known-issues-v2.md, docs/v2/installing.md, docs/v2/migration.md

Problem

This plugin only works on OpenCode v1. On v2 the v1 file does not load at all — a different plugin API, so the entrypoint fails with Cannot find package '@opencode/plugin'. Anyone who has moved to v2 has no auto-resume, and there is no partial option: the module either loads or it does not.

What the v2 API required

Concern v1 v2
Plugin shape hooks object + ctx.client { id, setup }, setup returns a cleanup fn
Events event hook, { type, properties } ctx.event.subscribe() AsyncIterable, flat { type, data }
Calls ctx.client.session.* over HTTP flattened onto ctx.session.*; no list/active, so discovery goes through callSessionApi with client-side verification
Assistant text session.messages() polling accumulated from session.text.delta, with ctx.session.context() as the authority
Logging ctx.app.log no log sink exists in v2 — the port writes its own log file

Two shape differences are worth naming because they would have made a detector inert rather than wrong, which is the harder failure to notice: AssistantTool carries its name in part.name with no callID, and the tool lifecycle is session.tool.called (increment) against .success/.failed (decrement).

Feature parity, and what running it live turned up

Every recovery path v1 has is here — stalled-stream watchdog, execution/step failure recovery, provider-retry awareness, tool-call-as-text detection, ready-to-continue and stalled-intent nudges, done-claim verification, the hallucination-loop guard, subagent and permission awareness, the visible recovery notification.

Parity was the bar. The first push was a port of the architecture rather than the behavior; running it live found the rest, one commit each. The ones that changed what the plugin does:

  • Unknown tool calls — v1 suggests the closest real tool name; the first v2 version silently ignored them.
  • Silent dead streams — a finished assistant message with no text part and enough output tokens is a stream that died mid-response. It is the case where every text-based check has nothing to read, so it is caught on the idle event rather than after the settle delay.
  • Premature stop — a turn that ends mid-thought without claiming completion, and activeUserWindowMs made real instead of decorative.
  • Context saturation — routes a saturated parent to magic-context ctx-wrapup, a saturated subagent to native compaction.
  • Todo list — read from the session message log (GET /api/session/<id>/message), because ctx.storage is namespaced per plugin and can never see another plugin's writes; ctx.storage.get("todos/<id>") survives only as a last-resort fallback. The open-todos nudge is gated on the list rather than on the model's emoji.

Since the above was written, six commits landed on this branch, all about the
todo list, its tests, and its docs — no behavior change except the first:

  • a83e158 — the todo read described above (message log first, storage last).
    Before it, the port read only ctx.storage, which is namespaced per plugin
    and can never see another plugin's list, so it concluded "no todos" while
    work was still listed and fired false done-claim nudges.

  • 801e22e, a355db3, 5f3fb0a — docs/comments only: why the storage key
    can never work cross-plugin, why the parser is a copy rather than an
    import, and the same correction in docs/known-issues-v2.md (which had
    contradicted the README) plus the test count 774 → 780.

  • ef25e39 — test only: one intermittent failure in 9 runs
    (index.options.test.ts, expected >= 2 nudges got 1). The test pushed two
    idles 10ms apart with a 0ms pattern delay and assumed the first timer fired
    inside the gap; under load the second idle replaced the first pass (the
    replace-don't-stack semantics above, working as designed) instead of
    following it. The test now gates on the observed nudge. Product code
    untouched.

  • f773ea6 — docs only: README and docs/known-issues-v2.md now name opencode-todo-fork as the confirmed todo plugin; others unconfirmed.

  • Explicit completion — a task_complete tool, so "done" is a signal instead of a string to pattern-match.

  • Reasoning-tool recovery — a tool call written into reasoning is asked for again properly.

  • Orphan parent recovery — a parent left waiting on a subagent that will not answer, which no amount of nudging fixes.

  • The settle delay — see below.

Every option is now applied

FEATURE_GATED_OPTIONS is empty. Earlier pushes accepted options the port did not yet act on, and the startup line reported them as accepted-but-inert=…. The last of those was toolTextCheckDelayMs, and the reason it took longest is that its absence was a real bug, not a plumbing gap.

v1 never judges a finished turn on the session.idle event. It arms a timer and runs the done/tool checks from there, because session.idle can arrive while the assistant's closing text is still being written into the message history — a check that reads too early sees a half-finished turn. The v2 port judged on the idle event, using the live delta buffer, which usually has the final turn by then and is empty when the plugin loads mid-turn. The fallback in exactly that case is the history that may not have flushed.

So inspectOnIdle is now two passes:

Pass When Decides
structural on session.idle dead stream, user hand-off, user recently active
pattern + toolTextCheckDelayMs celebration, tool-call-as-text, ready-to-continue, action intent, done-claim

The pattern pass re-reads the history — that is the point — and re-runs the structural guards rather than trusting the first verdict, because three seconds is long enough for the user to have replied. A new turn cancels a pending pass, a second idle replaces rather than stacks, and cleanup drops any pending timer.

The inert-reporting machinery is kept rather than deleted. It is the only signal a user gets that an option is being ignored, so with nothing left to report at runtime, a test asserts the invariant on the source instead: a test fails if an option joins the recognized set without being implemented.

How the port is tested

188 tests across 13 files, one per feature area, driven through the real event stream with a harness that reproduces v2's ordering. The gotchas that cost time are documented in the tests, because each one produces a failure that looks like a logic bug:

  • a finished assistant message with no text part is a dead stream and fires first, so reasoning and unknown-tool fixtures need an explicit text part
  • an idled session must be removed from the harness's active set, and a reset needs a full execution.started → step.started → step.ended → idle cycle
  • a per-request re-arm derived from history cannot run before the history read — a real ordering bug, found this way

The settle-delay file is mutation-checked: removing the delay, the arming, the history re-read, either guard re-run, the cancel latch, the new-turn cancel or the cleanup each turn it red. Two of its tests were vacuous on the first pass and are only meaningful because of that check.

docs/known-issues-v2.md and the README's option list are checked against FEATURE_GATED_OPTIONS in both directions, so a documented-but-unimplemented option cannot pass quietly.

Installation

Nothing to install first. src/v2/index.ts imports only node:fs, node:os and node:path and defines its own { id, setup } helper, so a local-file install needs no bun add — copy the file into ~/.config/opencode/plugins/ (or <project>/.opencode/plugins/) for auto-discovery, or register it in config:

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

@opencode/plugin@2.0.5 is a typecheck dependency, not a runtime one. Full steps and a v1→v2 migration checklist are in docs/v2/.

The install guide had been telling users to bun add @opencode/plugin@2.0.5 because the beta port did import the package. This one does not, so that step is gone, along with the troubleshooting row describing the Cannot find package '@opencode/plugin' failure this build cannot produce. docs/v2/installing.md also had chunkTimeoutMs at 45000 in three places and a 12-test count that stopped being true a while ago.

Relationship to the other open PR

The companion PR ("Stop the recovery-continue infinite loop") fixes v1's retry-counter reset and adds an OOC lock to src/index.ts. This port is independent — a separate module, unaffected by that patch — and carries the same exceeds the available context size lock-out, plus maxRetries and a continueTimestamps loop guard.

master was merged into this branch (27 commits ahead). The only hand-resolved conflict was two rows in the README option table, where both sides had rewritten them; the port's version is kept, because it is the one that also covers the subagent case.


The deployed v2 build running on the author's box is a build of this module; this PR is its source of truth.

valentimarco and others added 8 commits August 25, 2026 12:28
- Add src/v2/index.ts: full port using Plugin.define + ctx.event.subscribe()
- Add docs/v2/migration.md: migration notes and API mapping
- Update README.md: v2 callout banner and install instructions

Ports from v1 hooks-object API to v2 promise-plugin API:
- Event pump via AsyncIterable instead of event hook callback
- Flattened SDK calls (session.prompt({sessionID, text}))
- Assistant text accumulated from streaming events (no message history access)
- New guards: permission-aware pausing, subagent-aware waiting, failure-recovery arming
- All detection/recovery features preserved: stall watchdog, failure recovery,
  tool-call-as-text, ready-to-continue nudges, hallucination loop guard, etc.

Tested against opencode2 v0.0.0-beta-18050, @opencode-ai/plugin 0.0.0-next-17403.
…ilure

Replace session.prompt() with session.synthetic() which injects a visible
message into the session timeline (shown in TUI) so the user knows the
plugin intervened. The synthetic message also resumes the session when
resume=true, replacing the separate prompt call.

Notification text per recovery path:
- "auto-resume: stalled — retrying" (watchdog stall)
- "auto-resume: recovering: <kind>" (tool-text, ready-to-continue, etc.)
- "auto-resume: abort+resume escalation" (loop guard / max retries)

Falls back to session.prompt() if synthetic is not available (beta compat).
12/12 typecheck + smoke tests pass.
- import from @opencode/plugin (stable package; @opencode-ai/plugin was beta-only)
- read authoritative assistant text from ctx.session.context() at idle time,
  keeping the delta accumulation for liveness
- pass an AbortSignal to ctx.event.subscribe() and abort it on cleanup
- honour session.execution.interrupted data.reason (only "user" disables recovery)
- drop the beta-era nested synthetic({ path, body }) fallback
- document session.reverted as v1 legacy (v2 emits session.revert.*)
- docs/v2/installing.md: step-by-step v2 install (drop-in + plugins entry),
  options reference, verification and troubleshooting
- docs/v2/migration.md: verified stable-vs-beta delta (package rename, event
  surface, new session methods, subscribe signal) and updated test notes
- README: point at the new install guide and stable API
A local .ts plugin is imported by opencode with normal ESM resolution and the
plugin dependency graph is resolved at server startup, so @opencode/plugin must
be installed in the config dir (~/.config/opencode) and opencode restarted.
Without it the log shows "failed to load plugin ... Cannot find package
'@opencode/plugin'".
Mirrors the v1 src/index.ts fix onto the v2 port:
- oocLocked / oocLockReason / selfRecovery on SessionWatch
- OOC_ERROR_RE (exceeds the available context size / too large to
  compact / too many tokens / prompt is too long)
- maybeLockOoc latches on retry.scheduled + failed events carrying an
  OOC error; recover() and tryAbortAndResume refuse while locked
- execution.started from a genuine (non-self-recovery) run clears the
  lock; self-recovery busy edges don't reset the retry counter

v1 full suite on this branch: 578 pass / 0 fail (v1 source untouched).
v2 build: clean.
Root cause: recovery injects a synthetic 'continue' turn to a session
even while it is BUSY; in v2 a new turn interrupts the in-flight step
('Step interrupted'). The 45s stall watchdog falsely fires during long
27B thinking and native compaction (little/no text.delta), so in-flight
compaction was interrupted 3x back-to-back.

FIX A (compaction guard):
- SessionWatch.compacting / compactionStartedAt set on
  session.compaction.started, cleared on ended/failed and on
  execution.interrupted (feature-tolerant: no-op if the runtime
  emits no such events), stale-cleared after 30 min
- refuse recovery in recover(), tryAbortAndResume(), targetedRecovery();
  watchdog skips stall detection while compacting

FIX B (live-busy inject guard):
- at inject time, skip the synthetic turn if the session is still
  busy AND live within the chunk window: a working session is not
  stalled; the watchdog re-arms if it truly stalls
@famewolf

Copy link
Copy Markdown
Contributor Author

While landing the v2 "Step interrupted" fix I checked the v1 plugin (src/index.ts, v1 API) to see whether it needed a back-port. It does not — v1 is not affected by this bug — and I wanted to record why so the PR history is clear:

  1. Different prompt model. v1 sends recovery turns with ctx.client.session.prompt, which does not interrupt a session's in-flight step when a new turn lands. v2's ctx.session.prompt does — that is the whole source of the "Step interrupted" error. v1 cannot hit this failure mode.
  2. v1 already had the guard v2 was missing. v1 carries the continue-loop hard-stop (a maxRecoveryRetries cap plus a gaveUp latch that re-arms only on a genuine user message). The v2 port had dropped that; the OOC-lock commit in this PR (0a0a961) restores it. So the one place v2 needed to match v1 is already done.
  3. The new v2 guards are net-new. The compaction guard and the inject-time live-busy guard (9a44699) address a v2-only failure mode. v1 has no compacting flag and no compaction/inject guard — not because it's less careful, but because its prompt model can't interrupt an in-flight step, so it never needed them.

Bottom line: v1 is unaffected; no back-port to v1. The v2 fix (9a44699) is v2-specific and is what this PR carries (already on this branch).

A turn that ends with a question or an explicit prompt is awaiting the
user's reply. Nudging it (a synthetic 'continue') starts an in-flight
step that the user's slow reply then interrupts -> 'Step interrupted'.
Add isUserHandoff() and gate inspectOnIdle() on it. Purely additive;
legit nudges (tool-as-text / ready-to-continue / non-hand-off
done-claim) are unchanged.
@famewolf

Copy link
Copy Markdown
Contributor Author

Added a hand-off guard in 0dd0403:

isUserHandoff() detects a turn that cleanly ends by handing control back to the user — a question, or an explicit prompt — and inspectOnIdle() now skips its targeted continue nudge for such a turn.

This closes the last source of "Step interrupted": previously the plugin could inject a synthetic continue right after a hand-off turn (its last assistant message matched a tool-call/done-claim heuristic), starting an in-flight step; when the user then answered the question, their reply interrupted that step. A hand-off turn is, by design, awaiting the user, so it is never nudged now.

Purely additive — no change to the existing stall / compaction / OOC-loop guards. tsc clean; running on my box (md5 226d48ed63757983f4fd4c4512e30fcc).

…n a pending tool_use/question or recent user activity; + activeUserWindowMs option (default 15min)
famewolf added a commit to famewolf/opencode-auto-resume that referenced this pull request Sep 28, 2026
…e bursts

The plugin could interrupt a session and then recover from its own interrupt,
injecting a synthetic `continue` that superseded the step it had just killed.
The UI surfaces that as "Opencode failed to send message with error: Step
interrupted".

Root causes fixed here:

* Interrupt-shaped failures were never recognised. The filter only matched
  "interrupted by user" / a type containing "cancel", but v2 reports an
  interrupt as {type:"aborted", message:"Step interrupted"}, so the plugin
  recovered from its own abort forever. Added isAbortError() and made the
  plugin refuse to recover from any abort-shaped failure, its own or the
  user's.
* "Is this abort mine?" was tracked with a transient boolean that the runtime
  resets before it delivers session.execution.interrupted asynchronously.
  Replaced with a selfAbortAt latch + TTL, and the idle handler no longer runs
  its heuristics on top of our own abort.
* There was no single choke point for recovery injection: the stall watchdog,
  the intent/tool-loop nudges and the abort+resume escalation each called
  notifyAndPrompt directly, with no rate limit. Added injectOnce(), which all
  three paths now use, refusing injections inside our own abort window, into a
  busy-but-live session (a turn sent to a busy session supersedes its in-flight
  step), and more often than injectIntervalMs (15s). The abort+resume
  escalation takes its own lane, since swallowing its continue would leave a
  session interrupted with nothing to restart it.
* The loop guard counted the plugin's own continues, so it escalated on its own
  output; and targetedRecovery recorded the same continue twice. The guard is
  now evaluated before recording.
* getActiveSessions() returned [] because v2's plugin SessionDomain does not
  expose session.active, which made the subagent-wait guard permanently dead —
  a parent that went quiet while its child ran was declared stalled and
  interrupted. Falls back to busySessions(), derived from our own busy flags.
* Interrupts are now hard-capped per window, since an interrupt persists an
  errored assistant message that the UI shows as a send failure.

Also:

* Added the missing liveness events (session.step.streamed, *.started,
  session.synthetic, session.tool.input.*) so a step that starts and then
  produces no tokens, or a long tool-input generation, is not mistaken for a
  stall.
* Removed dead code: waitingOnSubagent (never set true, so its guard could
  never fire) and maxRecoveryRetries (read from options, never used).
* Made this file buildable: it imported a runtime Plugin.define from
  "@opencode/plugin", which is not a dependency of this repo. Use a local
  identity helper instead; `tsc --noEmit` now exits 0 where it previously
  failed with 2 errors.
* log() now prefers ctx.app.log, since plugin console output is not captured
  anywhere retrievable.

Verified with tsc (exit 0), the existing test suite (578 passing, 0 failing)
and a behavioral harness that asserts zero injections during healthy
streaming, zero after a user stop, zero in response to the plugin's own abort
events, and a working stall recovery path.
famewolf added a commit to famewolf/opencode-auto-resume that referenced this pull request Sep 28, 2026
- add row for 875d7e4: the v2 'Step interrupted' / synthetic-continue fix
- record that 875d7e4 is a strict superset of upstream PR Mte90#20 (b5fd8aa is a
  direct ancestor), so Mte90#37 already contains everything in Mte90#20
@valentimarco

Copy link
Copy Markdown

i will try the branch. Also on my code there was a bug where the tools (like questions) where interruped, will update if occurs again

famewolf added a commit to famewolf/opencode-auto-resume that referenced this pull request Sep 29, 2026
The v2 stand-down guard could never fire. It tested
`part.state.status === "pending"`, but the v2 message projection never
writes "pending" — an in-flight tool part reads "running" and settles to
"completed" or "error" (`pending` was the v1 spelling). With that branch
unreachable, auto-resume injected a synthetic `continue` over an
unanswered `question`, which surfaced as the tool call being interrupted
/ "Step interrupted". Reported in Mte90#33.

A second, independent defect: markIdle() cleared `permissionPending`
immediately before inspectOnIdle() consulted it. The `session.idle` case
calls markIdle() and then inspectOnIdle(), so the flag was always false by
the time the guard that honours it ran — dead on exactly the transition it
exists for. `permission.replied` is now the only thing that clears it,
plus a TTL stale-clear mirroring the compaction one, so a `permission.asked`
whose reply never arrives cannot strand a session forever.

Interactive tools (`question`, `permission`, `ask`, `confirm`) stand down
unless they reached "completed" — a `question` that came back "error" was
interrupted, not answered.

Verified against the real v2 message shape: ctx.session.context() returns a
plain array, and the tool-part states actually observed in a live session
are completed / error / running, never pending.

Tests: src/v2/index.stand-down.test.ts drives the real v2 seam
(event.subscribe -> async iterable, session.synthetic). It includes a
mandatory control asserting recovery DOES fire when nothing is pending,
plus over-blocking controls (answered question, non-interactive tool
erroring, permission asked-then-replied) so the fix cannot silently
disable auto-resume. 7 of its 13 assertions fail against the previous code.

Full suite: 591 pass / 0 fail. tsc --noEmit clean.
@famewolf

Copy link
Copy Markdown
Contributor Author

@valentimarco the question interruption bug you mentioned is fixed — it's in 67c34ff on this branch, so the checkout you're testing already has it.

There were two independent defects behind it, both in src/v2/index.ts:

1. The stand-down guard could never fire. shouldStandDownForUser tested

part?.state?.status === "pending"

but the v2 message projection never writes pending — an in-flight tool part reads running and settles to completed or error. pending was the v1 spelling. With that branch unreachable, auto-resume injected a synthetic continue over an unanswered question, which is what interrupted the tool call.

It now stands down on running/pending, and additionally on interactive tools (question, permission, ask, confirm) unless they reached completed — a question that comes back error was interrupted, not answered, so it still holds the turn.

2. markIdle() destroyed the permission guard before it was read. The session.idle case calls markIdle(sid) and then inspectOnIdle(sid), and markIdle set w.permissionPending = false. So the flag was always false by the time the guard that honours it ran — dead on exactly the transition it exists for. permission.replied is now the only thing that clears it, plus a TTL stale-clear mirroring the existing compaction one, so a permission.asked whose reply never arrives can't strand a session forever.

Two details worth knowing when reading the store, since both are easy to get wrong: ctx.session.context() returns a plain array, not { messages }. And across a live v2 session the tool-part states are completed 390, error 7, running 1 — no pending anywhere, which is what made the old condition look plausible in review.

Tests — src/v2/index.stand-down.test.ts drives the real v2 seam (ctx.event.subscribe async iterable → session.synthetic), so it asserts on real injections rather than grepping source. It carries a mandatory control that recovery does fire when nothing is pending — without it every other case passes vacuously — plus over-blocking controls (answered question, non-interactive tool erroring, permission asked-then-replied), so the fix can't silently disable auto-resume. 7 of its 13 assertions fail against the previous code. Full suite 591 pass / 0 fail, tsc --noEmit clean.

…tate

The port documented that v2 reverts surface as `session.revert.cleared` /
`session.revert.committed` and kept a defensive legacy `session.reverted`
case, but it never handled the v2 events themselves. On a real v2 runtime no
case matched, so a revert left the session's watch state — including any
armed recovery — intact against turns that had just been rewound.

Verified against the running opencode server: all three names are declared
event schemas with live store handlers.

- `session.revert.staged` is intermediate (the server sets a `revert` field
  there), so it only counts as activity and keeps the state.
- `session.revert.cleared` and `session.revert.committed` are terminal (the
  server clears that field on both) and drop the state, matching the existing
  `session.deleted` behaviour.

The legacy `session.reverted` case stays for v1-shaped runtimes.
@famewolf

Copy link
Copy Markdown
Contributor Author

Small addition rather than a change: the port's revert handling documents the
v2 event names but doesn't handle them yet, and this fills that in — your
session.reverted shim is left exactly as it is.

The switch has:

// v1 legacy: not emitted in v2 (reverts now surface as
// `session.revert.cleared` / `session.revert.committed`). Kept
// defensively so older runtimes still drop their watch state.
case "session.reverted": {

That comment names the two v2 events, but neither is handled anywhere in the
switch — on a real v2 runtime a revert matches no case, so the session's watch
state (including any armed recovery) survives the rewind of the conversation
it was tracking.

Checked against a running v2 server — all three are declared event schemas with
live store handlers:

type:"session.revert.staged"      case "session.revert.staged":    … "revert", C.data.revert
type:"session.revert.committed"   case "session.revert.committed": … "revert", void
type:"session.revert.cleared"     (same shape)

staged is intermediate — the server holds a revert payload there, so the
user can still clear or commit it. committed and cleared are terminal; the
server clears the field on both.

The change

Adds the three events alongside your existing case (which stays byte-for-byte
untouched):

case "session.revert.staged": {
    const sid = sidOf(ev)
    if (!sid) return
    touch(sid)   // staging is activity, not terminal — keep the state
    return
}
case "session.revert.cleared":
case "session.revert.committed": {
    const sid = sidOf(ev)
    if (!sid) return
    sessions.delete(sid)
    return
}

Terminal stages drop the state, consistent with how the port already handles
session.deleted. staged only touches, so a revert the user then abandons
doesn't needlessly tear down the watch. The commit is purely additive.

tsc --noEmit is clean. The new cases only ever fire on v2 event names, so
the shim's behaviour is unchanged and the watchdog logic is untouched.

If you'd rather keep this PR to the port itself, happy to split it into its
own PR — it's one self-contained commit with no interaction with the rest of
the port.

@famewolf

Copy link
Copy Markdown
Contributor Author

One more v1-parity item for this port.

The v1 plugin detects subagent sessions and excludes them from recovery. Two
places in src/index.ts on master:

  • handleEvent (~L2675):
    w.isSubagent = typeof parentID === "string" && parentID.length > 0
  • startTimer (L2247): if (w.isSubagent) continue — a subagent is never the
    target of the stall "continue" prompt.

The v2 port dropped both. Consequence: a child session (spawned as a subagent)
is indistinguishable from the parent, and both injection paths here —
injectOnce() and tryAbortAndResume() — can target it. That matters because a
child never reads the parent's AGENTS.md, so a "stand down on injected
continues" rule in the parent gives it no protection once the prompt arrives as
a task turn in the child, mid-task.

I have the port-side fix on my fork, branch local/v2-subagent-guard:
famewolf/opencode-auto-resume@pr20-v2port...local/v2-subagent-guard
(three commits, all additive on top of this PR's head). It re-adds the
parentID-based skip at the top of both injection paths, tracks open shells from
the shell.created/shell.exited registry events so a busy parent isn't
force-resumed, and aligns the stall window with v1
(DEFAULT_CHUNK_TIMEOUT_MS 45s → 180s — proposed for v1 master as a separate,
independent PR).

Happy to either fold this into PR #37 as a follow-up commit or open it as its
own PR stacked on pr20-v2port — whichever you prefer.

famewolf added a commit to famewolf/opencode-auto-resume that referenced this pull request Sep 30, 2026
…syStallStrategy

Additive to the existing v2 port on this branch — no rebase, no changes to
src/index.ts. The v1 side of the plugin is untouched here.

The v2 port read 15 of v1's 31 options and silently ignored the other 16, so a
v1 config ported to v2 quietly ran on defaults. Three changes close that:

- All 31 v1 option names are now read. `maxRecoveryRetries` is accepted as a
  fallback for v2's `maxRetries`, so a v1 config needs no edits.
- 8 of them are accepted but not applied, because the v2 feature they tune is
  not ported (no token read, no reachable todo state, no discovery sweep).
  They are listed as accepted-but-inert in the startup line and explained in
  docs/known-issues-v2.md rather than left to look like live behaviour.
- Any option key this build does not know produces one warn line at startup.
  v1 dropped unknown keys silently, which is how the gap went unnoticed.

busyStallStrategy is now complete rather than half-wired: "off" skips the
stall and "abort" interrupts the wedged step before continuing, mirroring the
v1 branch. Previously only "off" was handled, so "abort" silently behaved as
"continue" while appearing in the recognised set.

src/v2/index.options.test.ts covers all of the above behaviourally, with a
control per group, plus a source contract that fails if the declared options
type and the recognised set drift apart. 630 pass, 0 fail.

Also mirrors two upstream changes the v2 build had not picked up:
  - e1b8374: targetedRecovery now counts an attempt only after the prompt is
    delivered, so a rejected send no longer burns a retry.
  - DEFAULT_ACTIVE_USER_WINDOW_MS 15 min -> 5 min, matching v1.

build:v2 was missing from package.json, so dist/v2/ could not be rebuilt from
this branch at all. Added, along with dist/v2/index.js in files.
…t all

Two features ported from v1, and one defect fixed that made the plugin
unobservable.

Session discovery. v1 swept session.list() on an interval and trusted a
`status` field on each row. v2 has something better — session.active() is the
server's own record of what is running — so the sweep seeds a watch for every
existing session and adopts the running ones as busy. Without it, a turn that
was already mid-flight when the plugin attached produced no
`session.execution.started` for us to see, so the stall watchdog had nothing
to inspect and a wedged session stayed wedged. Both calls are read
defensively: the v2 plugin `session` domain is a narrowed Pick that omits
`list` and `active`, so each falls back to the raw client, and a host that
supplies neither degrades to the event-derived busy set rather than failing.
`discoveryDelayMs` is live again as a result, so it leaves the
accepted-but-inert list (7 remain, down from 8).

Logging. v1 logged through `ctx.client.app.log({ body: {...} })`, a real
endpoint that reached the opencode log file. v2 removed it — `ctx.app` is
`{name, version, channel}`, there is no `app` group in the protocol, and a
hosted plugin's console.log is not captured by OpenChamber. The v2 build was
therefore completely silent: the deployed one had zero log lines, and its only
startup line predated the current work by days. A running watchdog and a dead
one were indistinguishable, which is exactly what made a stall look like
"nothing happened". This build appends to
`~/.local/state/opencode-v2/auto-resume.log` (override with the new `logFile`
option or `AUTO_RESUME_LOG_FILE`), capped at 2 MB, every write best-effort so
an unwritable directory can never break the watchdog. Console output is kept
alongside it.

src/v2/index.discovery.test.ts covers both API shapes with a control per group,
the `{data}` envelope, malformed rows, a throwing list(), and teardown leaking
no timers. The option tests now read the log file rather than a stubbed
`ctx.app.log`, so they exercise the real sink. 639 pass, 0 fail.

Docs follow the code: known-issues-v2.md drops the stale discoveryDelayMs row
and documents both features, and README's activeUserWindowMs row now says
plainly that v1 on this branch is still 15 min while v2 is 5 min.
The stall watchdog only inspects busy sessions, so a turn that ends cleanly
with the work unfinished is invisible to it by construction — no stall, no
error, no streaming failure, just idle. Two v1 detectors for that case are
ported onto the idle path.

A done-claim with no work report in it. v2 gated the details prompt on a
400-character threshold, which cannot tell "Task done." from a real two-line
summary: it over-nudged terse reports and let short real ones pass. It now
uses v1's containsWorkDescription — a backticked or bare path with a dotted
extension, or a report header (Changed / Verification / Tests run / Results /
Commands run). Asking again after a genuine report loops forever (Mte90#26).

A trailing 🎉. The model's own "finished" signal, so completion latches
instead of being nudged over. Per-turn: a new turn re-opens the question, and
a later idle with no new text does not re-derive it forever.

That exposed a second defect in activeUserWindowMs. The default was lowered to
5 minutes to match e1b8374, but the implementation asked "is the newest message
a user message?" instead of v1's "was any inbound user message inside the
window?". At idle time the newest message is the assistant turn that just
finished, so the option never applied and the plugin nudged straight over a
user who was mid-conversation. It now walks back for the most recent user
message.

dbg() also goes to the log file now, for the same reason the rest of the
logging does: debug output is exactly when you are working out what the plugin
did, and console-only output in v2 reaches nobody.

src/v2/index.premature-stop.test.ts, 17 tests: a control per group, plus the
two halves of the length heuristic, one-nudge-per-turn, budget re-arm,
maxRetries, latch survival, latch reset, the handoff and active-user guards,
and the session.context() fallback. 656 pass, 0 fail.
…does

A session can fill its context window without ever looking stalled — it just
keeps working until it chokes. v1 caught that on idle by comparing the token
count against the model's usable window. The v2 port declared the option inert
because there was nothing to compare against; there was, it just needed reading.

Tokens: session.usage.updated carries the session's current usage.
tokenTotalOf sums input + output + reasoning + cache.read + cache.write, which
is exactly TokenUsage.total in the schema and exactly what v1 accumulated.

The window: ctx.model.get(providerID, modelID) hands over Model.Info.limit
directly, where v1 had to walk the raw provider list. Usable is
limit.context - Math.min(20_000, limit.output) — v1's arithmetic, unchanged, so
the same threshold means the same thing on both builds. Cached per model.

Routing is v1's. A subagent, with subagentNativeCompactionEnabled, requests
native compaction: v1 called session.summarize(), v2 spells it session.compact,
and it is not on the plugin session Pick, so it goes through the same defensive
lookup that carries discovery. A parent goes to magic-context's ctx-wrapup
command, but only when magic-context is installed — its setup disables native
compaction, so compacting here would double-compress. v1 detected that with
config.get().plugin; v2 removed the config domain, so it now reads
ctx.plugin.list() and matches both the plugin id and its source spec, which is
what makes it work for a local path and a package target alike.

Both paths are one-shot per turn and fail-safe: no limit, no token count, a user
cancellation, or a signalled completion means no intervention.

contextSaturationThreshold and subagentNativeCompactionEnabled are live, so they
leave the accepted-but-inert list (5 remain, down from 8). 16 new tests with a
control per group, covering both routings, both compaction transports, the
package-spec and local-path magic-context shapes, the one-shot budget, the user
interrupt guard, the reserve arithmetic, cache and reasoning in the total, the
threshold as a fraction, and the missing-model and missing-inventory fallbacks.
672 pass, 0 fail.
@famewolf

famewolf commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

Five commits on this branch fix the todo-nudge path and align it with the
associated todo plugin. Only the first changes behavior; the rest are docs,
comments, and one test fix. Code references are to src/v2/index.ts on this
branch.

a83e158 — read todos from the session message log (the behavior fix)

The port read the list only from ctx.storage.get("todos/<sid>"). That key
is namespaced per plugin (storage/plugin/<PLUGIN-ID>/<key>.json, route
/api/plugin/storage/<PLUGIN-ID>/<key>), so a todo plugin writing the same
key lands in its own directory — auto-resume was reading a key nothing wrote,
concluding "no todos" while work was still listed, and firing false
done-claim nudges at sessions with open items.

readTodos() now tries three sources in order: ctx.client.session.message. list when the host offers it, GET /api/session/<id>/message?limit=200
over loopback (200 is the server ceiling; the endpoint answers {data, cursor}, newest first), and ctx.storage last as a fallback. The parser
(todosFromMessages()) keeps every completed todowrite input — todowrite
replaces the whole list, so the newest completed call IS the current list —
ranked by timestamp, because page order is newest-first and last-match-wins
would select the oldest list. Returns undefined (never []) when nothing
was found, so "no todos" stays distinguishable from "could not look". Covered
by src/v2/index.todo.test.ts (+143 lines), mutation-checked.

801e22e — correct the reason the storage key cannot work

a83e158's own comment said the key was unused because "nothing in v2 writes
it" — wrong reason, and it invites the wrong fix (writing the key too, which
would race the todo plugin as a second writer). The real reason is the
per-plugin namespace above. README + code comment rewritten; behavior
unchanged.

a355db3 — say why the parser is a copy, not an import

The parser mirrors the todo plugin's todosFromMessages. Not imported on
purpose: no cross-plugin import contract exists, and importing would tie
auto-resume's todo reading to a sibling installed at a fixed path.
Comment-only; both copies carry the ordering note and both repos test it.

5f3fb0a — finish the docs correction

801e22e fixed the README but docs/known-issues-v2.md still described the
old storage read, contradicting the README in the same branch — rewritten to
match — plus the stale 774 passing count → 780 in docs/v2/migration.md.
Docs only.

ef25e39 — fix one intermittent test (test only)

bun test failed 2 runs in 9 (index.options.test.ts: expected ≥ 2 nudges,
got 1). A test race, not product: two idles 10ms apart with a 0ms pattern
delay assumed the first timer fired inside the gap, but under load the second
idle replaced the first pattern pass (the documented replace-don't-stack
semantics) instead of following it. The test now gates the second idle on the
observed first nudge. Product code untouched; suite is 780/0 with tsc --strict clean.


The other half: /todo and the sidebar list (associated todo plugin)

The nudges above consume a list owned elsewhere. The associated plugin
(opencode-todo-fork) owns it three ways, all reading the same message log:

  • /todo command — prints the session list on demand: Todo [done/total], completed marked, the in-progress item named as the current
    task. Reads the log first (todosFromMessages, timestamp-ranked like
    above), ctx.storage fallback second.
  • Sidebar strip — the OpenCode CLI task list panel, built from
    src/tui.tsx with data from src/tui-data.ts latestTodosFromMessages.
    Header reads Todo [done/total] · %; it consumes the SDK-ordered store
    (oldest-first), for which last-match-wins is the correct selection.
  • todowrite / todoread tools — the writers. todowrite replaces the
    whole list per call, which is why "newest completed call IS the list" holds
    for every reader.

What /todo prints (live output from the author's session):

Todo [0/2] - 2 open
Current task: Verify /todo shows this list after server restart
  DOING 1. Verify /todo shows this list after server restart
  OPEN  2. Report round-trip result to user

The relationship is consumer/owner, not two owners: auto-resume never writes
a list, only reads; the todo plugin never nudges, only displays. The message
log is the single store both sides agree on — that shared dependency is why
these commits are on this PR rather than the todo repo.

These bug fixes are both on github and backed up to gitea.

Ported from f99ddd8 (auto-resume/v2-todo-message-log): stacked watchdogs
each hold private counters, so the in-memory skip cannot see a sibling's
prod — the session log can. Test coverage lands in
src/v2/index.duplicate-prod.test.ts (index.visible-continue.test.ts no
longer exists on this branch).
Ported from d74debe (auto-resume/v2-todo-message-log). Test-only delta vs
the original: none — src/v2/index.duplicate-instance.test.ts and
src/v2/index.inject-lock.test.ts apply as-is. Docs hunk dropped
(docs/v2/backport-to-v1.md no longer exists on this branch).
…uations

Ported from d756146 (auto-resume/v2-todo-message-log). Deliberate delta
vs the original: the ready line keeps only the mod= instance tag — the
visibleContinue= interpolation is dropped because the visibleContinue
option does not exist on this branch. src/v2/index.singleton-registry.test.ts
applies as-is.
…er fires into active work

Ported from 2047d7c (auto-resume/v2-todo-message-log): applies cleanly,
no deltas. 13 dead-stream tests pass.
…match

Ported from 808fb80 (auto-resume/v2-todo-message-log): whitespace-only
conflict at the ensureWatch initializer resolved to HEAD indentation.
18 unknown-tool tests pass.
…to the ban

Ported from aaac082 (auto-resume/v2-todo-message-log). Deliberate delta
vs the original: the inert configProbe option (interface, setup const,
RECOGNISED_OPTIONS, README row) is NOT ported — it belongs to the excluded
probe/docs scope. 6 rate-limit tests pass.
…ject recency

Ported from 9cd4471 (auto-resume/v2-todo-message-log): applies cleanly,
no deltas. 19 unknown-tool tests pass.
Ported from 7dca80d (auto-resume/v2-todo-message-log): applies cleanly,
no deltas. 20 unknown-tool tests pass.
Ported from a69376a (auto-resume/v2-todo-message-log). Deliberate delta
vs the original: the rich stall-text builder (buildStallContinueText)
does not exist on this branch, so recover() keeps its plain continue
text — only the parent-wait guard (detection + inject time) is ported.
3 parent-wait tests pass.
…gistry

Ported from b2fb63f (auto-resume/v2-todo-message-log) — required companion
to the d74debe+d756146 ports: the structural test asserted the module-scope
singleton that d756146 replaced with the globalThis registry (covered by
index.singleton-registry.test.ts instead).
Ported from 8073ba1 (auto-resume/v2-todo-message-log). Deliberate deltas:
kept HEAD's newer injectOnceLocked signature (required params from the
mutex port) and HEAD's ownPromptTexts tracking; src/v2/index.visible-
continue.test.ts restored with its 7 tests (all pass).
…channel

Ported from a2d3897 (auto-resume/v2-todo-message-log): ready line keeps
both tags (visibleContinue + mod= instance tag from the globalThis port).
8 visible-continue tests pass.
Ported from bf4e8c8 (auto-resume/v2-todo-message-log): applies cleanly,
no deltas. 10 visible-continue tests pass.
Ported from 6e20464 (auto-resume/v2-todo-message-log): applies cleanly,
no deltas. 13 stand-down tests pass.
Ported from af87ab6 (auto-resume/v2-todo-message-log): applies cleanly,
no deltas. inject-lock + singleton-registry suites now write a private
tmpdir logFile instead of the production default.
Ported from 2f99b9b (auto-resume/v2-todo-message-log) minus the
configProbe knob row (probe scope excluded): dead-stream paragraph +
silentDeadStreamMinTokens row + visibleContinue plugins-key note +
known-issues dead-stream amendment.
… stub)

Ported from b0b5100 (auto-resume/v2-todo-message-log): restores
docs/v2/backport-to-v1.md verbatim ( HEAD had deleted it). Line-numbered
research mapping v2 features onto v1's src/index.ts.
v2 suite is 233 tests across 20 files (was: 188 across 13); feature list
now names the ported areas (dup suppression, mutex/singleton, quota
ladder, parent-wait, visible channel). Figures from executed runs,
2026-10-05.
…out touching a live knob

Ported from 1064641 (auto-resume/v2-todo-message-log): applies with
neighborhood merges only. README row included (was stripped from the
earlier quota/docs ports pending this decision). 23 options tests pass.
famewolf added a commit to famewolf/opencode-todo-fork that referenced this pull request Oct 5, 2026
Removes SUPERSEDED.md: the fork is NOT retired and is NOT superseded.
This branch is the live todo provider auto-resume reads — session list
via the message log (namespaced storage fallback) backing the v2 port's
done-claim verification, open-todos reminders, and task_complete
blocking (upstream PR Mte90/opencode-auto-resume#37).
ses_ef72c5f3 2026-10-05: the watchdog injected a rich stall continue
into a session parked on a question tool awaiting the user (186s event
silence during the Q&A round-trip). The busy-stall path had no user-wait
guard — only the idle path stands down. New isWaitingOnUser() covers
only user-awaiting tools (question/permission/ask/confirm), so a wedged
bare tool still recovers; wired at detection (checkActiveSessions) and
inject time (recover). Covered by src/v2/index.user-wait.test.ts (1
repro + 2 controls).
@famewolf

famewolf commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Hi @Mte90 — this pushes 20 commits onto the v2 port, all fixes proven live
on a box running the plugin daily. Each is a cherry-pick from our local
fix branch with tests; deltas vs the originals are noted where they exist.
Full suite: 822 pass / 0 fail, tsc --noEmit clean.

Stall correctness

  • 2052c07 cross-instance duplicate check via session log — stacked watchdogs
    (config-reload churn) hold private counters, so the in-memory skip can't see
    a sibling's prod; the shared session log can. Fail-open.
  • 3c7ce7f busy-stall stands down while a turn waits on the user — the
    watchdog injected a rich stall continue into a session parked on a
    question tool (186s event silence during the Q&A round-trip; the busy
    path had no user-wait guard, only the idle path did). New isWaitingOnUser
    covers only user-awaiting tools, so wedged bare tools still recover.
  • 7fb1b9f one live instance per process + per-session inject mutex — setup()
    re-runs on every config reload produced six identical continues in the same
    millisecond; the mutex serializes check-then-act per session.
  • 4ccbf80 singleton on globalThis — module scope can't see bundle
    re-evaluations (one pid, fresh module identity per reload).
  • 8a34aa5 thinking/tool-call turns are alive — the dead-stream detector no
    longer fires into turns carrying tool calls or with tools in flight
    (fired twice into active work before this).
  • c36c560 skip busy-stall recovery while a parent waits on live subagents —
    a quiet parent blocked on a worker got spurious continue — stalled
    injections (3 hits, observed live); the parentID link is now consulted at
    detection and inject time.

Nudge budgets

  • 7fdc472 own prompts don't re-arm budgets; bash→shell alias before fuzzy
    match — injected continues read back as user messages and re-armed the
    budgets they just spent.
  • 0fef903 + 05838f7 re-arm exclusion survives null-body projections;
    re-arm keeps examined parts so stale errors never re-suggest.

Quota

  • 333a4e1 quota failures stand down on a gate ladder (~12h) instead of
    retrying into the ban; no timers, so reloads lose nothing; zero budget
    consumed. Why: both OpenCode Zen and most online models carry quota limits
    that can interrupt a task at any time. Without this, a session that hit
    quota sat overnight without getting processed — every failure burned a
    retry into the ban wall. Observed live 2026-10-01T19:51:28Z:
    …vbPCjpH7 session.step.failed: provider.quota Rate limit exceeded
    (186 failure lines in seconds, then silence). The ladder backs off and
    retries after a suitable timeframe instead.

Visible channel

  • 67437d8 visible stall continue + rich prod text (reason/attempt/todos) +
    in-memory exact-duplicate skip; 90113a7 AUTO_RESUME_VISIBLE_CONTINUE
    env fallback + channel in the startup line; 3df2339 visible defaults true
    (config-parser drops plugin-entry options); 0497165 stand-down CONTROL
    follows the visible default. Rationale for the channel change: this keeps
    v2 at parity with v1's visible text. Injected synthetic messages are
    visible to the model but not to the user — when the model started
    responding to something that wasn't in the transcript, there was no way
    to notice a looping error or even tell why it was answering. A real
    prompt message makes every intervention (and any loop) self-evident in
    history; synthetic remains only as the fallback if prompt() throws.

Hygiene / docs

  • a38d681 drops the module-scope assertion superseded by the globalThis
    registry; a9f2425 keeps test sessions out of the live log;
    7cc0714 inert configProbe delivery probe; 963a9fd dead-stream docs;
    e89bd0b v1-backport research doc; 27add46 refreshes the v2 suite
    figures (233 tests / 20 files).

Deliberately NOT included: the temporary 20s/180s stall-threshold
experiments (net-zero; 180s stands) and the configProbe bits that leaked
into two earlier commits (folded into 7cc0714 instead).


Why a todo-plugin fork exists at all (not spelled out in the PR body)

The body documents the read side (message log first, namespaced storage as
fallback) but never the reason a fork was necessary, so stating it here: v2
removed the todowrite/todoread tools, which took with it both the todo
list and every v1 nudge gated on it (done-claim verification, open-todos
reminders, task_complete blocking). opencode-todo-fork (forked from
opencode-todolist-local) was adjusted to work inline with auto-resume: it
writes the session list where the port can actually read it — the session
message log first, namespaced todos/<sessionID> storage as fallback — so
the port recovers the full v1 nudge set instead of concluding "no todos"
and firing false done-claim nudges. The confirmed-plugin naming (f773ea6)
is the tail of that story; this is the head.


What the model actually sees (nudge mockups)

Sample scenario used below: 3 open todos. All texts verbatim from
src/v2/index.ts; only the todo contents are illustrative.

1. Rich stall continue (default, richContinuePrompt: true)

continue — stalled (no activity for 188s; attempt 1/3).
Remaining todos:
• [in_progress] Fix the gluetun proxy bypass in private windows
• [pending] Verify FoxyProxy routes private traffic via 192.0.2.10:3128
• [pending] Write up findings

2. Bare continue (richContinuePrompt: false)

continue

3. Open-todos reminder (done-claim with open todos / celebration false positive)

You have 2 unfinished tasks:
1. [pending] Verify FoxyProxy routes private traffic via 192.0.2.10:3128
2. [in_progress] Fix the gluetun proxy bypass in private windows

Please continue working on these tasks.

4. task_complete blocked (open todos remain)

Mark any finished todos complete and do not redo completed work.
You have 2 unfinished tasks:
1. [pending] Verify FoxyProxy routes private traffic via 192.0.2.10:3128
2. [in_progress] Fix the gluetun proxy bypass in private windows

Please continue working on these tasks.

5. Done-claim with no work description

Your last response claimed the task is complete but contained no work description. This is not acceptable. You MUST respond now with a full, detailed report of everything you did: for each file you modified, state the full path and the exact changes; list every command you ran to verify and its result; state the final outcome. Do NOT reply with 'done', 'task completed', or any short acknowledgment — your ONLY acceptable response right now is this detailed report. Write it now.

6. Done-claim with no work detected

I need you to verify more carefully that you have actually completed all the required tasks. Your response indicated you're done, but no work was detected. Please check your todo list and complete any remaining work.

7. Tool call printed as text

Your last message contained a raw tool call printed as text instead of being executed. Please use the proper tool calling mechanism to execute it.

8. Tool call left in thinking/reasoning

I noticed you have a tool call generated in your thinking/reasoning. Please execute it using the proper tool calling mechanism instead of keeping it in reasoning.

9. Tool loop

I notice you've been calling the same tool multiple times in a row without making progress. Please step back and reassess your approach. Consider: 1) Are you stuck in a loop? 2) Do you need different information first? 3) Should you try a different tool or break the task into smaller steps? Take a moment to think about what's blocking you and propose a different strategy.

10. Unknown tool (with / without a close match)

You tried to use the tool "bash" 2 times, but it does not exist. The closest matching tool is "shell". Please use "shell" instead and adjust your arguments accordingly. Available tools include: shell, read, write, edit, ….
You tried to use the tool "frobnicate" 2 times, but it does not exist. Please check the available tools and use the correct one. Available tools include: shell, read, write, edit, ….

11. task_complete acknowledgement (1st / 2nd / 3rd call)

Task completion acknowledged. No further continuation will be sent. End your turn now with a brief text reply — do not call task_complete or any other tool again unless the user sends a new message.
Task completion acknowledged already on your previous call — completion is recorded. End your turn now with a brief text reply. Do not call task_complete again; further repeat calls are rejected as errors.
(task_complete already acknowledged twice … → third call throws as an error, forcing the turn to end)

12. Stuck-subagent recovery

It looks like you may have stalled or timed out. Please retry the last operation or continue with the task.

@Mte90

Mte90 commented Oct 6, 2026

Copy link
Copy Markdown
Owner

if we include a fork of another plugin we have to check the license and mention it in the readme

@famewolf

famewolf commented Oct 6, 2026 via email

Copy link
Copy Markdown
Contributor Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants