Skip to content

fix(tui): guard prompt submit against concurrent invocation#26972

Merged
kitlangton merged 2 commits into
anomalyco:devfrom
kitlangton:fix/prompt-submit-race
May 12, 2026
Merged

fix(tui): guard prompt submit against concurrent invocation#26972
kitlangton merged 2 commits into
anomalyco:devfrom
kitlangton:fix/prompt-submit-race

Conversation

@kitlangton
Copy link
Copy Markdown
Contributor

Summary

Two submit() calls in packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx can overlap (most commonly a double-pressed Enter). Both pass the if (!store.prompt.input) return false guard, both await sdk.client.session.create(...), and both only read inputText = store.prompt.input after that await. The first call finishes, sends its prompt, then clears the store; the second call, now past its await, reads "" and sends an empty prompt to a second freshly-created session.

The user observes:

  • An orphaned session (visible only by browsing) containing their actual text and a real assistant reply.
  • A phantom session — the one they navigate to — with an empty user message, an assistant reply (the model responds to nothing), and a hallucinated title (the title generator inferred from its own system prompt because the user content was empty).

Reproduced in a real session in the OpenCode DB: two session rows created 3ms apart, one with text: "Hello there." and one with text: "", sharing the same directory.

Fix

Wrap submit() in an in-flight submitting flag. The original body is preserved verbatim as submitInner(); only the guard is new.

let submitting = false
async function submit() {
  if (submitting) return false
  submitting = true
  try {
    return await submitInner()
  } finally {
    submitting = false
  }
}

Tests

packages/opencode/test/cli/tui/prompt-submit-race.test.ts is a structural mirror of submit(). The first commit adds it pointed at the buggy shape (no guard) and fails. The second commit adds the production fix and updates the mirror to match, after which both tests pass.

Caveat: this is a structural regression test, not an integration test against the real Prompt component. Mounting Prompt for a true E2E test would require building harnesses for ~14 contexts (sync, sdk, project, editor, dialog, toast, command-palette, keymap, tui-config, route, args, local, prompt-history, prompt-stash) plus a fake SDK client. Happy to do that as a follow-up if reviewers want it.

Test plan

  • bun run test test/cli/tui/prompt-submit-race.test.ts from packages/opencode (2 pass)
  • bun run typecheck from packages/opencode (clean)
  • Manual: in the TUI, type a message, double-press Enter quickly, verify exactly one session is created and contains the typed text

When two `submit()` calls in the prompt component overlap (e.g. a
double-pressed Enter), both pass the empty-input guard and both
`await sdk.client.session.create(...)`. Only after that await does
each call read `inputText = store.prompt.input`. The first call clears
the store after sending, so the second call reads `""` and sends an
empty prompt to a second freshly-created session — leaving an
orphaned session with the user's actual text and a phantom session
visible to the user with only an assistant reply.

This adds a structural reproducer mirroring the exact shape of the
production `submit()` and asserts that no submission may end up with
empty text. It fails on the current code path.
Wrap `submit()` in the Prompt component with an in-flight `submitting`
flag so overlapping invocations bail out instead of each creating a
session and racing the store-clear. The original body is preserved
verbatim as `submitInner()`.

Without this guard, a double-pressed Enter (or the input's native
onSubmit racing another dispatch) produces two sessions: one with the
user's text, and one with an empty user message that the title-gen
model labels from its own system prompt — leaving the user staring at
an assistant reply with no visible question.

Updates the regression test added in the previous commit to mirror the
guarded shape; it now passes.
@kitlangton kitlangton enabled auto-merge (squash) May 12, 2026 01:31
@kitlangton kitlangton merged commit 74aa735 into anomalyco:dev May 12, 2026
10 checks passed
leohenon pushed a commit to leohenon/opencode-vim that referenced this pull request May 12, 2026
sdeonvacation added a commit to sdeonvacation/opencode-x that referenced this pull request May 12, 2026
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