Skip to content

fix(web,hub): surface inactive-session error on text-only send (closes #918)#922

Open
heavygee wants to merge 1 commit into
tiann:mainfrom
heavygee:fix/web-inactive-send-error
Open

fix(web,hub): surface inactive-session error on text-only send (closes #918)#922
heavygee wants to merge 1 commit into
tiann:mainfrom
heavygee:fix/web-inactive-send-error

Conversation

@heavygee

Copy link
Copy Markdown
Collaborator

Summary

Closes #918.

Sending text via the web composer to an archived/inactive session
silently dropped on the floor: the hub returned 409 but the web client
swallowed the failure in resolveSessionId's catch branch with only a
console.error, leaving the operator with no toast, no inline failed-bubble,
and no Reopen affordance.

This change closes the visibility hole and adds a one-click recovery, with
no regression to the existing 5xx / network text-restore UX.

Changes

Hub (one line + a test): requireSession({requireActive: true})'s 409
body now carries a machine-readable code: 'session_inactive' next to the
existing error string. Lets the web client discriminate this branch from
other 4xx without string-matching the i18n-able human message.

Web:

  1. useSendMessage now fires onError on resolveSessionId rejection,
    not just on POST /messages failure. The mutation never started, but
    the operator still needs to see the typed text retained and a reason
    surfaced.

  2. The route component (router.tsx) classifies the thrown error: a 409
    with code: 'session_inactive', or a synthetic ApiError thrown from
    resolveSessionId, attaches a Reopen action to the existing inline
    composer-error affordance. Plain 4xx / 5xx / network keep the legacy
    text-restore UX untouched.

  3. The Reopen click calls api.reopenSession (matches SessionList's
    menu item), invalidates the session queries, and navigates to the
    resumed session id. Per the discovery brief's friction-pass on POST /reopen on archived cursor ACP session: spawned cursor-agent dies ~90s after successful ACP load, merges away the original (no recovery) #917
    (broken reopen path), it does not auto-replay the send -- the
    operator re-clicks Send on the restored composer text once Reopen
    lands. This keeps the failure-mode blast radius identical to the
    existing Reopen surface.

  4. HappyComposer's inline error region now renders an optional action
    button (data-testid='composer-send-error-action').

  5. chat.sendError.sessionInactive + chat.sendError.sessionInactive.action
    added to en.ts and zh-CN.ts.

Test plan

  • Hub test: messages.test.ts -- new test verifies the 409 body
    carries code: 'session_inactive'.
  • Hook tests: useSendMessage.test.tsx -- new tests cover
    `POST returns ApiError(409, 'session_inactive')` (onError carries
    the typed ApiError), `resolveSessionId rejects` (onError fires
    keyed by the original sessionId, closing the silent-drop hole), and
    `5xx still uses the legacy text-restore path` (no regression --
    code is null, classifier falls through to the verbatim error
    message).
  • `bun typecheck` + `bun run test:hub` + `cd web && bun run test`
    all green locally.
  • Playwright smoke against the soup driver: mocked the 409+code
    response, sent from the composer, asserted inline error + Reopen
    button visible within 1s of click. Screenshot in
    `localdocs/playwright-runs/peer-b-918-handoff.png` (operator-local,
    not committed).

Out of scope

AI disclosure

Implemented by an AI agent (Claude Opus 4.7) acting on operator
instructions per CONTRIBUTING.md. Operator reviewed the patch and the
Playwright smoke output before opening this PR.

Made with Cursor

…tiann#918)

Sending text via the web composer to an archived/inactive session
silently dropped on the floor: the hub returned 409 but the web client
swallowed the failure with a console.error in the resolveSessionId catch
branch, leaving the operator with no signal and no recovery path.

Hub: add a machine-readable `code: 'session_inactive'` to the 409 body
so the web client can discriminate this branch without string-matching
the i18n-able human message.

Web (router.tsx, useSendMessage.ts, HappyComposer.tsx):

  - useSendMessage now fires `onError` on resolveSessionId rejection,
    not just on POST /messages failure -- closes the visibility hole
    when the inactive session has no resume target or resume itself
    fails.

  - The route classifies the thrown error: a 409 + session_inactive
    code or a synthetic ApiError thrown from resolveSessionId attaches
    a Reopen action to the existing inline composer-error affordance.
    Plain 4xx / 5xx / network keep the legacy text-restore UX
    untouched.

  - Reopen calls api.reopenSession (the same path as SessionList's
    Reopen menu item), invalidates the session queries, and navigates
    to the resumed sessionId.  Per the orchestrator brief's friction
    pass on tiann#917 the affordance does NOT auto-replay the send; the
    operator re-clicks Send on the restored composer text.

Tests:

  - hub messages.test.ts: 409 carries `code: 'session_inactive'`.
  - useSendMessage.test.tsx: ApiError(409, session_inactive) from POST
    flows through onError; resolveSessionId rejection flows through
    onError keyed by the original sessionId; 500 keeps the legacy
    fallback path with no code attached.

AI disclosure: implemented by an AI agent (Claude Opus 4.7) acting on
operator instructions; tests pass locally (bun typecheck + bun run
test for hub and web).

Co-authored-by: Cursor <cursoragent@cursor.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • None.

Summary

  • Review mode: initial
  • No issues found in the latest diff. Residual risk: local validation could not be run in this runner.

Testing

  • Not run: bun is not installed in this runner (bun: command not found).

HAPI Bot

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.

web: text-only send to inactive session silently restores composer text with no error UI; operator perceives 'message disappeared'

1 participant