Skip to content

fix: restore automatic goal continuation on OpenCode V2 - #46

Merged
danyel117 merged 1 commit into
mainfrom
fix/v2-execution-lifecycle
Sep 5, 2026
Merged

fix: restore automatic goal continuation on OpenCode V2#46
danyel117 merged 1 commit into
mainfrom
fix/v2-execution-lifecycle

Conversation

@danyel117

Copy link
Copy Markdown
Contributor

Summary

  • Handle native V2 execution start/success/failure/interruption and scheduled retries, while preserving legacy idle events.
  • Schedule a delayed wake-up when a fast successful turn finishes inside the minimum continuation interval.
  • Keep interrupted/non-transport-failed executions stopped until the host starts another execution; preserve native retry precedence and bounded transport recovery.
  • Scope server-wide events to the owning plugin location while still observing foreign child-task lifecycle.
  • Add public-interface regression tests and a deterministic isolated native V2 smoke runner.

Verification

  • lint, typecheck, 248 Bun tests, build, pack dry-run, and diff check pass.
  • Native OpenCode V2 beta-19086: /goal creates the objective, two automatic continuations run with the default 3-second minimum interval, and the goal completes. Two locations loaded; no duplicate delivery.
  • Negative control: exact npm 0.1.45 installed in the same isolated test fails with zero automatic continuations.
  • No live goals resumed, shared server restarted, or provider credentials used.

Release

User requested publication and installation after validation. Publishing uses the existing main-branch GitHub Actions workflow; exact npm version will be smoke-tested again after release.

@danyel117
danyel117 merged commit b7e185c into main Sep 5, 2026
4 checks passed
spencer2211 added a commit to spencer2211/opencode-goal-plugin that referenced this pull request Sep 7, 2026
A Task child session that never reports a terminal state, or whose terminal
result is never reconciled by an orchestrator turn, could defer goal
continuation forever. Bound it with a `max_task_block_seconds` ceiling
(default 900, `0` to disable), measured from `runningSince` for a listed
child and from `terminalAt` for an unreconciled terminal one.

`markTerminal` now carries the original `terminalAt` when it re-marks the same
still-unreconciled terminal state, along with the assistant marker captured at
that moment. `refreshLiveChildren` re-marks a listed idle child on every poll,
so rewriting the timestamp each time meant the terminal branch of the ceiling
could never fire for the case it exists to bound.

The deferral also validates the goal before re-arming. The retry runs at 1 Hz
and writes nothing to the goal, so a goal closed, cleared, or paused while a
child still blocks would otherwise keep polling until the ceiling - and
forever with `max_task_block_seconds: 0`.

Rebased onto b7e185c (prevalentWare#46).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MLdQ88tCqSgfEsaGR978UF
danyel117 pushed a commit that referenced this pull request Sep 7, 2026
* fix: bound task-deferred goal continuation

A Task child session that never reports a terminal state, or whose terminal
result is never reconciled by an orchestrator turn, could defer goal
continuation forever. Bound it with a `max_task_block_seconds` ceiling
(default 900, `0` to disable), measured from `runningSince` for a listed
child and from `terminalAt` for an unreconciled terminal one.

`markTerminal` now carries the original `terminalAt` when it re-marks the same
still-unreconciled terminal state, along with the assistant marker captured at
that moment. `refreshLiveChildren` re-marks a listed idle child on every poll,
so rewriting the timestamp each time meant the terminal branch of the ceiling
could never fire for the case it exists to bound.

The deferral also validates the goal before re-arming. The retry runs at 1 Hz
and writes nothing to the goal, so a goal closed, cleared, or paused while a
child still blocks would otherwise keep polling until the ceiling - and
forever with `max_task_block_seconds: 0`.

Rebased onto b7e185c (#46).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MLdQ88tCqSgfEsaGR978UF

* fix: match the task-deferral predicate to reserveContinuation's rules

`taskDeferralGoalContinuable` treated every `budgetLimited` / `usageLimited`
goal as continuable. That holds only until the single wrap-up is reserved:
`reserveWrapup` returns null once `budgetWrapupSent` is set, so a child still
blocking after the wrap-up had been sent kept the 1 Hz poll re-arming with no
possible continuation - indefinitely with `max_task_block_seconds: 0`.

The predicate now mirrors the reservation rules instead of restating them: a
limited goal is continuable only while its wrap-up is unspent, and every other
non-active status fails `canContinue` outright.

Lifecycle coverage is now symmetric. V1 and V2 each cover cleared, paused,
completed, and the two-legged wrap-up case. Leg A of that pair is the control:
a predicate that simply refused every non-active status would pass leg B while
silently dropping the wrap-up a blocked limited goal is still owed. Both wrap-up
tests also assert the deferral returns after `update_goal_status active` clears
`budgetWrapupSent`, proving the loop stayed reachable and only the predicate
was holding it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MLdQ88tCqSgfEsaGR978UF

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant