fix(web,hub): surface inactive-session error on text-only send (closes #918)#922
Open
heavygee wants to merge 1 commit into
Open
fix(web,hub): surface inactive-session error on text-only send (closes #918)#922heavygee wants to merge 1 commit into
heavygee wants to merge 1 commit into
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 aconsole.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 409body now carries a machine-readable
code: 'session_inactive'next to theexisting
errorstring. Lets the web client discriminate this branch fromother 4xx without string-matching the i18n-able human message.
Web:
useSendMessagenow firesonErroronresolveSessionIdrejection,not just on
POST /messagesfailure. The mutation never started, butthe operator still needs to see the typed text retained and a reason
surfaced.
The route component (
router.tsx) classifies the thrown error: a 409with
code: 'session_inactive', or a syntheticApiErrorthrown fromresolveSessionId, attaches a Reopen action to the existing inlinecomposer-error affordance. Plain 4xx / 5xx / network keep the legacy
text-restore UX untouched.
The Reopen click calls
api.reopenSession(matchesSessionList'smenu 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.
HappyComposer's inline error region now renders an optional actionbutton (
data-testid='composer-send-error-action').chat.sendError.sessionInactive+chat.sendError.sessionInactive.actionadded to
en.tsandzh-CN.ts.Test plan
messages.test.ts-- new test verifies the 409 bodycarries
code: 'session_inactive'.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).
all green locally.
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
sessionCache: renameSession / clearSessionArchiveMetadata / restoreSessionArchiveMetadata throw on version-mismatch without refresh; forever-409 until out-of-band cache refresh #919) is being handled by a sibling peer on a separate worktree.
through the existing Reopen endpoint as-is. If Reopen fails the route
shows a toast with the underlying message (mirrors
SessionList'sReopen behavior).
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