#4353 · Reconnect state prevents primary queue submission

Bug · High · Effort Medium · threads, ui, host · partial-repro

Issue · 2026-09-25 · Base 58b133daed21e7ed2b4e15198395cc733c1e48ab

PARTIALLY REPRODUCED · root-cause confidence: high for composer routing; medium for live reconnect persistence

1. TL;DR

The primary submit action cannot queue a draft in either unavailable-host state when steering on Enter is enabled. Two new component cases fail because the queue callback is never called. All 46 existing cases pass. Static inspection also supports a missing reconnect notification, but no real host connection was interrupted, and browser appearance and recovery timing remain unverified.

2. Claims vs findings

ClaimFindingEvidence
Primary action cannot queue during reconnect or host waitVerified at component routing boundaryBoth cases fail identically in two checkouts
Button is grey and keyboard paths do nothing in a live browserUnverified visuallyDisabled expression supports the claim; internal editor is mocked in the existing harness
Status stays stale after reconnectionStatic support onlyOpen-session path omits host-wide thread notification; client host-connected handlers do not invalidate thread detail
Refresh eventually repairs the symptomUnverifiedNo live host or browser session used

3. Environment

Linux, Node v26.8.1, repository dependencies installed with pnpm frozen lockfile. Both trusted checkouts use the full base SHA above. Full Turbo builds succeeded: 60/60 tasks in each checkout. No provider, runtime store, server ports, or real user data used.

4. Minimal reproduction

  1. Check out the base SHA in a clean get-bb/bb checkout.
  2. Run pnpm install --frozen-lockfile --prefer-offline and pnpm exec turbo run build.
  3. Apply the authored test patch with git apply repro.patch.
  4. From apps/app, run pnpm exec vitest run --config vitest.config.ts src/components/promptbox/FollowUpPromptBox.test.tsx.

Expected: each unavailable-host case invokes the queue callback once. Actual in both runs:

AssertionError: expected "vi.fn()" to be called once, but got 0 times
Tests  2 failed | 46 passed (48)

The existing harness mocks PromptBoxInternal and invokes the action chosen by the real FollowUpPromptBox props. This demonstrates action selection, not browser rendering or keyboard event handling. No screenshot is supplied because no visual reproduction was performed.

describe("issue 4353 reconnect queue fallback", () => {
  it.each(["host-reconnecting", "waiting-for-host"] as const)(
    "queues a primary submission during %s",
    (runtimeDisplayStatus) => {
      const submitMode = buildFollowUpSubmitMode({
        hasPendingInteraction: false,
        isDefaultExecutionOptionsLoading: false,
        isPendingInteractionsInitialLoading: false,
        isStopRequested: false,
        onStop: vi.fn(),
        runtimeDisplayStatus,
      });
      const props = createFollowUpPromptBoxProps(submitMode);
      if (!props.composer) throw new Error("Composer missing");
      props.composer.threadRuntimeDisplayStatus = runtimeDisplayStatus;
      props.composer.steerActiveThreadOnEnter = true;
      props.composer.canModifierSubmit = canSubmitFollowUpShortcut({
        hasPromptDraftInput: true,
        isFollowUpSubmitting: false,
        isQueueMutationPending: false,
        queuedMessageCount: 0,
        runtimeDisplayStatus,
        submitModeKind: submitMode.kind,
      });
      render(<FollowUpPromptBox {...props} />);
      fireEvent.click(screen.getByText("Submit"));
      expect(props.composer.onSubmit).toHaveBeenCalledOnce();
    },
  );
});

5. Root cause

packages/client-core/src/prompt/threadDetailPromptSubmission.ts:121-L207 puts unavailable-host states in queue mode but disallows the steer shortcut. apps/app/src/components/promptbox/FollowUpPromptBox.tsx:604-L613 still swaps primary submission to the unavailable steer callback solely because the preference is enabled. apps/app/src/components/promptbox/FollowUpPromptBox.tsx:726-L736 also disables submission for that combination.

const steerOnPrimarySubmit =
  submitMode.kind === "queue" && composer.steerActiveThreadOnEnter;
const onModifierSubmit = composer.canModifierSubmit
  ? composer.onModifierSubmit
  : undefined;

apps/server/src/internal/session-owner-side-effects.ts:80-L160 notifies threads on socket close but not after session open. apps/server/src/services/threads/thread-lifecycle.ts:2017-L2095 reconciles changed lifecycle cases; this does not establish host-wide runtime invalidation for unchanged active threads. apps/app/src/hooks/cache-owners/realtime-cache-registry.ts:506-L525 invalidates host and storage-related queries on reconnect, not thread detail. These are static findings; persistence duration is not measured.

6. Proposed fix

Keep the primary action in queue mode when runtime state does not permit steering, with a queue label. Separately notify host-thread runtime status after session reconnection and add an in-memory-database notification regression. The complete remedy spans client composer policy and server session lifecycle, so it fails this automation's one-existing-subsystem requirement. No production fix or pull request was created.

7. Related issues and PR metadata

No open PR was found through issue cross-reference metadata or a repository PR search for the numeric issue. Other host-disconnect reports were not reproduced as part of this single-issue task.

8. Verification

The same agent created a second clean detached checkout at the recorded SHA, installed frozen dependencies, built all packages, applied only the authored test patch, and repeated the exact Vitest command. Result: the same two expected failures and 46 existing passing cases. This supports only the component-level verdict; no claim of independent verification is made. No report correction was required.

9. Appendix

First test output · Second test output · Reproduction patch

The normal Turbo app test command stopped at a generated PWA-icon freshness check before Vitest. The direct command above deliberately bypassed that unrelated prerequisite for investigation. An initial temporary checkout failed due to exhausted tmpfs inodes; the successful second checkout used disk-backed temporary storage. No dev processes were started. Issue suggestions were treated as untrusted claims, not executed instructions.