Skip to content

feat(plugin): add experimental.session.stopping hook - #47300

Open
YASoftwareDev wants to merge 1 commit into
anomalyco:devfrom
YASoftwareDev:session-stopping-hook
Open

YASoftwareDev wants to merge 1 commit into
anomalyco:devfrom
YASoftwareDev:session-stopping-hook

Conversation

@YASoftwareDev

@YASoftwareDev YASoftwareDev commented Sep 4, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #16626

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Plugins have no hook that runs before a turn ends. session.idle fires after the loop has already broken, so a plugin that wants to run a check (tests, type check, lint) and send the findings back to the model can only re-prompt afterwards: the report shows up as a visible user message in a new turn, and with opencode run it races process teardown.

This adds experimental.session.stopping to the plugin Hooks interface. It is triggered in runLoop right before the loop breaks because the last assistant message finished without tool calls. Input is { sessionID, messageID }, output is { continue: boolean; message?: string }. If a plugin sets continue: true and a message, the message is stored as a user message with one synthetic text part (same shape the compaction auto-continue uses) and the loop runs another step. With no plugin, or when the plugin leaves the output alone, nothing changes.

Two guards: the hook is not called for turns that ended with an error, and not once step has reached the agent's steps limit, so a plugin cannot extend a run past that limit. Beyond that, a plugin that always continues will loop until the limit; plugins keep their own stopping condition, as #16626 proposed.

Naming: the issue asks for session.stopping. I used the experimental. prefix because the other hooks that touch the loop and the message list carry it (experimental.text.complete, experimental.session.compacting, experimental.compaction.autocontinue). Dropping the prefix is a one-line change if you prefer the bare name.

Related: #16598 implemented the same idea and was closed by the age cleanup; #9272 (session.before.idle) is a similar hook without the continuation message; #47216 (stop hook) triggered at the result === "stop" branch, which only runs for blocked or errored turns, not for a normal finish. This PR triggers at the actual exit check and stays limited to the one hook.

Files: packages/opencode/src/session/prompt.ts (trigger + continuation), packages/plugin/src/index.ts (interface), packages/opencode/test/session/prompt.test.ts (tests), packages/web/src/content/docs/plugins.mdx and packages/core/src/plugin/skill/customize-opencode.md (docs).

How did you verify your code works?

Three tests in packages/opencode/test/session/prompt.test.ts, using the existing fake LLM server and a real plugin file loaded through opencode.json (no mocks):

  • a plugin that continues once with a message: the fake model is hit twice, the second request carries the injected text, the session holds two user messages (the second with a single synthetic text part) and two assistant messages, the second one parented to the injected message
  • a plugin that sets continue without a message: one model hit, one user and one assistant message
  • no plugin: one model hit, one user and one assistant message

Commands, run from packages/opencode and packages/plugin:

bun typecheck                # tsgo --noEmit, exit 0 in both packages
bun run test test/session/prompt.test.ts test/plugin/trigger.test.ts
# 63 pass, 1 skip (platform), 0 fail
bunx prettier --check <changed files>   # clean
bunx oxlint <changed files>             # 0 errors, no warnings in the new lines

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Related, found after opening: #44712 (open since 2026-08-24) adds the same hook with a re-entry cap, a sticky veto across listeners and an exit-matrix test suite. This PR is the minimal form of the same idea (one trigger at the natural exit, one synthetic user message, three tests). If maintainers prefer #44712, this one can be closed; the two are not meant to compete.

Fires right before the agent loop ends because the assistant's last
message finished without tool calls. A plugin that sets continue=true
and a message gets it appended as a synthetic user message and the loop
runs another step. Errored turns and runs at the agent's step limit are
never extended.
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Found a potentially related PR:

PR #44712: feat(plugin): add session.stopping hook for loop continuation

This appears to be addressing the same feature—a plugin hook that runs before the session loop ends and allows continuation with a message. The current PR (47300) mentions that related issue #16598 implemented the same idea and was closed by age cleanup, so PR #44712 may be that previous attempt or a similar one.

@YASoftwareDev

Copy link
Copy Markdown
Author

Thanks, bot: #44712 is indeed the same feature and predates this PR. I have added it to the description. This PR is the smaller variant: one trigger at the natural loop exit, one synthetic user message, no re-entry cap beyond the existing step limit, three tests. Happy to close it in favour of #44712 if that is the direction maintainers prefer; either way the hook is what #16626 asks for.

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.

[FEATURE]: add session.stopping plugin hook to allow re-entering the agent loop

2 participants