Skip to content

fix(genui): stop GenUI reminder from triggering an extra turn - #56548

Open
kitlangton wants to merge 2 commits into
betafrom
fix/genui-extra-turn
Open

kitlangton wants to merge 2 commits into
betafrom
fix/genui-extra-turn

Conversation

@kitlangton

@kitlangton kitlangton commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Why

In the OpenCode app, sending a first prompt with Generated UI enabled (metadata.genui = true) caused the assistant to reply twice to a single prompt. In session ses_eb4805506ffdExxfN3euuGJzSR, the user sent "hi" at 1792268020571, assistant 1 completed at 1792268021922, a synthetic message containing the GenUI <system-reminder> was delivered at 1792268021930, and assistant 2 started at 1792268021939 and greeted the user a second time.

GenUISession's context hook both spliced Message.user(enter) into event.messages and called ctx.session.synthetic({ sessionID, text, resume: false }). Because Session.synthetic admits a "steer" inbox item and resume: false only skips execution.wake(), the already-active runner promoted the synthetic item in advanceToStep after step 1 finished, running an extra model step and persisting the reminder after the assistant's reply (diverging from the order the model saw).

What Changes

Make the GenUISession context hook side-effect free by providing GenUI guidance as a system part (event.system.push(SystemPart.make(...))), matching packages/core/src/tool/plugin/opencode.ts:

  • Before: On a context hook call, GenUISession scanned event.messages for enter/leave <system-reminder> user messages, spliced a user message into event.messages, and called ctx.session.synthetic({ sessionID, text, resume: false }), queuing a steered synthetic message while a step was already running.
  • After: The prompt hook records whether the latest prompt had metadata.genui === true (and session.deleted cleans up the map entry). On context, if GenUI is offered for the session, GenUISession pushes SystemPart.make(instructions) onto event.system and keeps genui_render; otherwise it deletes genui_render from event.tools and adds nothing. It never mutates event.messages or enqueues synthetic inbox items.
sequenceDiagram
  participant A as App
  participant R as Runner
  participant H as GenUISession (context hook)
  participant M as Model

  Note over A,M: Before
  A->>R: prompt ("hi", metadata.genui = true)
  R->>H: context hook
  H->>R: splice Message.user(enter) + session.synthetic(resume: false)
  R->>M: step 1 (user: enter, user: "hi")
  M-->>R: greeting 1
  R->>R: advanceToStep promotes steered synthetic item
  R->>M: step 2 (persisted synthetic reminder)
  M-->>A: greeting 2

  Note over A,M: After
  A->>R: prompt ("hi", metadata.genui = true)
  R->>H: context hook
  H->>R: event.system.push(SystemPart.make(instructions))
  R->>M: step 1 (system includes GenUI guidance, user: "hi")
  M-->>A: greeting 1
Loading

Scope

This PR keeps the fix small in packages/plugin-genui/src/session.ts using existing event.system support on the context hook, plus unit and runner regression tests. A separate follow-up PR will move GenUI onto keyed session instruction entries once plugins can access them.

Verification

# packages/plugin-genui
taskpolicy -c utility bun test test/session.test.ts
skulk run --slot heavy --stable 0s -- taskpolicy -c utility bun run typecheck

# packages/core
taskpolicy -c utility bun run script/test.ts test/session-runner.test.ts -t "runs one model request with system guidance when a prompt enables GenUI"
skulk run --slot heavy --stable 0s -- taskpolicy -c utility bun run typecheck
  • Targeted GenUI session tests: 4 pass, 0 fail. Cover enabled guidance on repeated context calls, disabled and missing metadata, and unknown sessions; messages remain unchanged and no synthetics are enqueued.
  • Runner regression: 1 pass, 0 fail. Uses the real tool registry and verifies exactly one model request, with both genui_render and its system guidance, only "hi" in user input, and no persisted reminder.
  • Both affected package typechecks and formatting checks pass.
  • Repository commit/push hooks also passed their checks, including the full typecheck graph; existing lint warnings remain.

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant