Skip to content

fix(tui): publish synthetic reject event when permission/question ask is interrupted#29352

Open
sjawhar wants to merge 1 commit into
anomalyco:devfrom
sjawhar:fix/tui-permission-question-orphan-reject
Open

fix(tui): publish synthetic reject event when permission/question ask is interrupted#29352
sjawhar wants to merge 1 commit into
anomalyco:devfrom
sjawhar:fix/tui-permission-question-orphan-reject

Conversation

@sjawhar
Copy link
Copy Markdown

@sjawhar sjawhar commented May 26, 2026

Issue for this PR

Closes #28312

Type of change

  • Bug fix

What does this PR do?

When a permission or question ask is interrupted (tool cancel, session end, fiber kill), the cleanup finalizer used to just remove the pending entry without publishing anything. The TUI subscribes to Event.Replied/Event.Rejected to dismiss prompts, so without that final event the prompt stayed visible forever and Enter did nothing because the underlying entry was already gone.

This adds a synthetic reject publish inside the finalizer, but only when the entry is still in pending — the reply path deletes the entry before publishing its event, so this only fires on the orphan path and never races with a real reply.

How did you verify your code works?

  • New live test in test/permission/next.test.ts that forks ask(), interrupts the fiber, and asserts a reject event was published.
  • Equivalent live test in test/question/question.test.ts.
  • bun typecheck + focused tests pass locally.

Checklist

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

When the underlying "ask" effect for a Permission or Question prompt is
interrupted (tool cancelled, session ended, parent killed, scope torn down)
rather than completing via a normal reply/reject, the Effect.ensuring
finalizer deleted the pending entry but never published a corresponding
terminal bus event. The TUI tracks prompt state via these events, so the
orphaned request stayed in sync.data.permission / sync.data.question
forever. Pressing Allow/Reject/typing a custom answer / pressing Esc all
issued SDK calls against a requestID that the server no longer had in
pending — the handler logs "reply for unknown request" and returns 200
OK silently with no event, so the TUI never dismissed and the prompt
became completely unresponsive (every option re-rendered the same view,
even exit was unreachable while the prompt held the input).

Fix: replace the unconditional pending.delete in the finalizer with an
Effect.gen that checks pending.has(id). The reply and reject paths delete
the entry themselves before publishing, so this guard fires only on the
interrupt path. When it does, delete the entry and publish a synthetic
terminal event so the TUI dismisses the orphan:

  - question/index.ts publishes Event.Rejected
  - permission/index.ts publishes Event.Replied with reply: "reject"
    (Permission has no dedicated cancellation event; "reject" is the
    accepted Reply value that signals dismissal)

Adds regression tests in both packages that fork the ask effect, wait
for it to be pending, interrupt the fiber, and assert the bus emits the
expected terminal event and the pending list is empty.
@github-actions
Copy link
Copy Markdown
Contributor

ghost commented May 26, 2026

Thanks for updating your PR! It now meets our contributing guidelines. 👍

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.

TUI permission dialog can become stale: Enter confirm does nothing while /permission returns

1 participant