fix: recover delayed session entry (#3631)
This commit is contained in:
@@ -28,49 +28,27 @@ A fast task session can change state or persist a reply while the browser is wai
|
||||
- **AC-PLATFORM-SESSION-SUBSCRIPTION-RECOVERY-001.7:** **GIVEN** one client-side consumer is already registering a session and a second consumer mounts, **WHEN** the second consumer requests hydration, **THEN** it waits for the shared registration acknowledgement and does not race an earlier `message.list` request ahead of registration.
|
||||
- **AC-PLATFORM-SESSION-SUBSCRIPTION-RECOVERY-001.8:** **GIVEN** a session subscription is restored after a WebSocket reconnect, **WHEN** the restored registration is acknowledged, **THEN** message reconciliation can run against the restored subscription and later events are delivered normally.
|
||||
|
||||
## Migrated source detail
|
||||
### REQ-PLATFORM-SESSION-SUBSCRIPTION-RECOVERY-002: Recover session entry from temporary delays
|
||||
|
||||
## Why
|
||||
**Intent:** Users can open an existing conversation during a temporary connection delay without a false startup failure or lost history.
|
||||
|
||||
A fast task session can change state or persist a reply while the browser is
|
||||
waiting for the backend to register its `session.subscribe` request. The live
|
||||
notification is then unavailable to that client, leaving the task looking busy
|
||||
or the transcript missing a reply until the user reloads the page.
|
||||
#### Acceptance criteria
|
||||
|
||||
## What
|
||||
- **AC-PLATFORM-SESSION-SUBSCRIPTION-RECOVERY-002.1:** A successful subscription or status response delayed by seven seconds shall complete session entry without a startup-failure alert.
|
||||
- **AC-PLATFORM-SESSION-SUBSCRIPTION-RECOVERY-002.2:** After a temporary entry timeout, the visible session shall retry automatically without a reload, navigation, or visibility change.
|
||||
- **AC-PLATFORM-SESSION-SUBSCRIPTION-RECOVERY-002.3:** On a continuously connected socket, unsuccessful automatic recovery shall stop within 45 seconds. The user shall then have an explicit Retry action.
|
||||
- **AC-PLATFORM-SESSION-SUBSCRIPTION-RECOVERY-002.4:** Until history loads successfully, an uncached conversation shall show loading, retrying, or unavailable feedback. It shall never show the empty-conversation invitation.
|
||||
- **AC-PLATFORM-SESSION-SUBSCRIPTION-RECOVERY-002.5:** A failed refresh shall preserve cached messages, drafts, and scroll position. Successful recovery shall clear its stale feedback.
|
||||
- **AC-PLATFORM-SESSION-SUBSCRIPTION-RECOVERY-002.6:** A status timeout shall identify a status-check problem, not a failed session start. Technical details shall remain collapsed by default.
|
||||
- **AC-PLATFORM-SESSION-SUBSCRIPTION-RECOVERY-002.7:** Recovery shall not duplicate agent launches or bypass archive, permission, or manual-start restrictions. Changing sessions shall cancel obsolete recovery effects.
|
||||
- **AC-PLATFORM-SESSION-SUBSCRIPTION-RECOVERY-002.8:** Desktop, phone, and task preview shall expose equivalent recovery outcomes. Feedback shall preserve chat access and keyboard focus without horizontal overflow.
|
||||
- **AC-PLATFORM-SESSION-SUBSCRIPTION-RECOVERY-002.9:** Phone and coarse-pointer recovery controls shall have touch targets of at least 44 pixels. Progress shall have a localized status announcement.
|
||||
- **AC-PLATFORM-SESSION-SUBSCRIPTION-RECOVERY-002.10:** Permission and missing-session failures shall stop automatic retries. A successful empty history response shall still show the normal empty-conversation invitation.
|
||||
|
||||
- A session subscription exposes a readiness point that resolves only after the
|
||||
backend has acknowledged registration.
|
||||
- Transcript reconciliation for a newly mounted or reconnected session starts
|
||||
after that readiness point, so the persisted message view covers the interval
|
||||
before registration and live notifications cover the interval after it.
|
||||
- Ref-counted consumers share an in-flight registration readiness point and do
|
||||
not unregister one another's subscription.
|
||||
- Reconnect and delayed-session retry paths use the same acknowledgement-aware
|
||||
registration behavior.
|
||||
- A failed or disconnected registration does not create an unhandled client
|
||||
rejection or leave a stale readiness promise that prevents the next
|
||||
connection from hydrating the session.
|
||||
## Implementation plans
|
||||
|
||||
## Scenarios
|
||||
|
||||
- **GIVEN** a browser sends `session.subscribe` while a session state change or
|
||||
reply is persisted before the backend registers the subscription, **WHEN** the
|
||||
backend acknowledges the subscription and the client reconciles messages,
|
||||
**THEN** the client displays the authoritative state and persisted reply
|
||||
without a page reload.
|
||||
- **GIVEN** one client-side consumer is already registering a session and a
|
||||
second consumer mounts, **WHEN** the second consumer requests hydration,
|
||||
**THEN** it waits for the shared registration acknowledgement and does not
|
||||
race an earlier `message.list` request ahead of registration.
|
||||
- **GIVEN** a session subscription is restored after a WebSocket reconnect,
|
||||
**WHEN** the restored registration is acknowledged, **THEN** message
|
||||
reconciliation can run against the restored subscription and later events are
|
||||
delivered normally.
|
||||
- **GIVEN** the registration fails or the socket disconnects before the
|
||||
acknowledgement, **WHEN** the client retries on the next connected socket,
|
||||
**THEN** the retry can establish a fresh readiness point and no stale promise
|
||||
or unhandled rejection blocks hydration.
|
||||
- [Acknowledgement ordering](../../../plans/session-subscription-recovery/plan.md)
|
||||
- [Delayed session entry](../../../plans/session-entry-recovery/plan.md)
|
||||
|
||||
## Out of scope
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
status: draft
|
||||
system: platform
|
||||
requirements:
|
||||
- REQ-PLATFORM-SESSION-SUBSCRIPTION-RECOVERY-001
|
||||
- REQ-PLATFORM-SESSION-SUBSCRIPTION-RECOVERY-002
|
||||
---
|
||||
|
||||
# Session subscription recovery
|
||||
|
||||
## Ownership and mapping
|
||||
|
||||
Platform owns shared session subscription readiness and recovery. This design extends that contract across chat, task preview, and session-status feedback.
|
||||
|
||||
| Requirement | Design |
|
||||
| --- | --- |
|
||||
| REQ-PLATFORM-SESSION-SUBSCRIPTION-RECOVERY-001 | Registration ordering and ownership |
|
||||
| REQ-PLATFORM-SESSION-SUBSCRIPTION-RECOVERY-002 | Entry recovery, history state, and presentation |
|
||||
|
||||
## Registration ordering and ownership
|
||||
|
||||
The gateway registers membership before acknowledging `session.subscribe` in `apps/backend/internal/gateway/websocket/client.go`.
|
||||
The frontend `WebSocketClient` owns reference counts and a shared readiness promise per session and connection generation.
|
||||
`fetchAndStoreMessages` in `use-session-messages.ts` waits for readiness before requesting history.
|
||||
This ordering remains mandatory. Recovery must not fetch history ahead of registration.
|
||||
|
||||
## Entry recovery
|
||||
|
||||
Use explicit 10-second deadlines for subscription registration and entry status checks. Keep the general request default unchanged.
|
||||
Add a typed timeout error in `apps/web/lib/ws/request-error.ts`, retaining its existing diagnostic message.
|
||||
Only this typed timeout permits the new automatic retry. Backend rejection codes retain their current meaning.
|
||||
|
||||
The client owns subscription retries within one shared readiness promise: two attempts, with one second between attempts.
|
||||
Do not increase reference counts for a retry. Concurrent consumers and unknown-session retries join an active registration episode.
|
||||
Only the current connection generation can settle readiness. Last unsubscribe cancels its timer and invalidates pending results.
|
||||
A disconnect cancels the episode; existing reconnect registration starts a fresh generation after reconnection.
|
||||
Do not queue episode retries while disconnected. Existing inactive-thread subscription limits remain unchanged.
|
||||
|
||||
The resumption hook separately retries only the `task.session.status` request with the same two-attempt policy.
|
||||
Keep `processResumeStatus` outside that retry operation. It runs once after a current successful status response.
|
||||
Never wrap resume, restore, launch, or prompt mutations in a retry loop.
|
||||
Preserve archive-state checks, request-identity guards, and existing resume eligibility policy.
|
||||
|
||||
After registration, history uses its existing 10-second request deadline and at most one additional timeout retry after one second.
|
||||
A continuously connected initial episode therefore takes at most 42 seconds: 21 for registration and 21 for history.
|
||||
Status progresses independently and takes at most 21 seconds. Automatic failure feedback appears within the 45-second requirement.
|
||||
Use timers with cleanup and fake-timer tests. Background throttling is outside the continuously visible timing guarantee.
|
||||
Foreground refresh and reconnect remain recovery triggers, but they join an active episode instead of resetting its budget.
|
||||
After exhaustion, explicit Retry starts a new bounded episode. Disable it while that episode is active.
|
||||
|
||||
## History state
|
||||
|
||||
Extend the existing message hooks with explicit load outcome and retry action: loading, retrying, ready, or unavailable.
|
||||
Keep this state associated with session identity and hydration generation. Share in-flight fetches across mounted consumers.
|
||||
The hook result supplies the state to `use-chat-panel-state.ts`, `task-chat-panel.tsx`, and the shared message-list components.
|
||||
A successful snapshot alone establishes history initialization, including a successful empty snapshot.
|
||||
|
||||
`doFetchMessages` in `use-session-message-fetch.ts` must not clear messages on rejection or mark failed hydration as successful.
|
||||
Preserve message reconciliation, prompt-history independence, draft state, and scroll anchoring.
|
||||
An obsolete response cannot overwrite another session or clear a newer episode's error.
|
||||
Partial live messages remain visible while initial reconciliation is pending.
|
||||
|
||||
## Presentation
|
||||
|
||||
Use the existing dedicated phone chat layout in `components/task/task-layout.tsx` as the mobile composition exemplar.
|
||||
Reuse the inline loading region in `components/task/chat/message-list-shared.tsx`.
|
||||
History feedback lives inside the chat region. Cached messages remain below a compact notice during recovery.
|
||||
The transcript remains the scroll owner. Do not add an overlay, drawer, or separate nested scroller for short feedback.
|
||||
The existing dynamic viewport and composer safe-area behavior remain intact.
|
||||
|
||||
Use localized copy such as `Loading conversation...`, `Taking longer than usual. Retrying...`, and `Conversation could not load.`
|
||||
For exhausted status checks, use `Session status is unavailable.` with Retry and a collapsed Details disclosure.
|
||||
Route entry-status feedback through a typed failure kind in `SessionRecoveryFeedback`, including task page and preview consumers.
|
||||
Do not globally rename actual launch failures in `describeEnsureError` or suppress provider, profile, and workspace recovery actions.
|
||||
A status-only failure must not replace the transcript loading result or hide usable cached chat.
|
||||
When both fail, show one history notice in chat and retain status details without a duplicate large banner.
|
||||
|
||||
Desktop actions use the normal 28-pixel control size. Phone or coarse-pointer actions use at least 44-pixel targets.
|
||||
Phone notices place actions below the message; desktop notices keep actions on the same row when space permits.
|
||||
Use one polite `role="status"` per visible operation and keep Retry's accessible name stable.
|
||||
Details use a keyboard-accessible disclosure with wrapped text. Do not move focus on automatic progress.
|
||||
All new copy uses existing i18n catalogs in five languages, with generated Traditional Chinese variants.
|
||||
|
||||
## Persistence, security, and observability
|
||||
|
||||
No database, protocol, public API, or permission changes are required.
|
||||
Existing server-side authorization and mutation admission remain authoritative.
|
||||
Retries preserve typed backend errors and expose only existing transport diagnostics in Details.
|
||||
Tests correlate request action and ID; they do not record chat payloads or credentials.
|
||||
The cause of the observed seven-second delivery delay is unconfirmed. This design does not claim to remove that underlying latency.
|
||||
|
||||
## Implementation plans
|
||||
|
||||
- [Original ordering repair](../../../plans/session-subscription-recovery/plan.md)
|
||||
- [Delayed session entry](../../../plans/session-entry-recovery/plan.md)
|
||||
Reference in New Issue
Block a user