Refactor Cookie-Mode Anonymous History Hand-off to Avoid Repeated /auth/link Probes
Context
Follow-up to #129.
#129 fixes the main anonymous → authenticated history hand-off flow in host_token / cookie authentication mode.
The important behavior is now covered:
Visitor starts anonymously
↓
Visitor signs in through the host application
↓
Opens conversation history
↓
Anonymous history is linked
↓
Authenticated history contains the previous conversations
This works without requiring the host application to explicitly call:
refreshIdentity()
reset()
However, the current implementation uses a forced identity check when conversation history is loaded.
Conceptually:
listConversations(...)
↓
current({ forceCheck: true })
↓
attempt /auth/link
This guarantees correct ordering after login, but introduces an unnecessary network probe while the user is still anonymous.
Problem
While an anonymous visitor still has a stored visitor pass, every forced history check may attempt:
POST /auth/link
↓
401 Unauthorized
↓
GET /conversations
Repeated history operations may therefore look like:
open history
→ POST /auth/link
→ 401
→ GET /conversations
load another page
→ POST /auth/link
→ 401
→ GET /conversations
refresh history
→ POST /auth/link
→ 401
→ GET /conversations
The main functionality is correct, but we are using failed authentication requests as a mechanism for detecting whether the host application's authentication state has changed.
This has several disadvantages:
- unnecessary network traffic;
- avoidable
401 responses;
- extra latency before conversation-history requests;
- authentication-state detection becomes coupled to a specific UI operation;
- the frontend is effectively trying to infer whether a session cookie became valid;
- the lifecycle becomes more complicated through
forceCheck, cookie snapshots, focus/visibility events, and retry state.
Core Design Problem
In cookie authentication mode, the widget does not own the authenticated credential.
The browser may have an authenticated session cookie, potentially an HttpOnly cookie, but the widget cannot reliably determine from JavaScript whether that cookie represents a valid authenticated session.
This means the frontend does not have a reliable event equivalent to:
authentication changed from anonymous → authenticated
Trying to reconstruct that state through:
timeouts
polling
document.cookie
focus events
visibility events
forced /auth/link attempts
is inherently indirect.
The server, however, already has the authoritative information.
For every request it can determine:
Who authenticated this request?
Goal
Make anonymous-history adoption in cookie mode deterministic without requiring the widget to repeatedly probe /auth/link.
We should preserve all of the following:
- Anonymous usage should not generate repeated
/auth/link requests.
- No timeout, sleep, or arbitrary retry interval should be required.
- No host-side
refreshIdentity() call should be required for the normal zero-code cookie flow.
- Once the browser becomes authenticated, the first request that depends on account history must observe the merged history.
- The merge must remain idempotent.
- Existing bearer/token-based hand-off flows must continue to work.
Suggested Direction: Server-Side Opportunistic Hand-off
Instead of asking the frontend to determine whether authentication changed, consider letting the server perform the hand-off when both pieces of information are available on the same request:
authenticated principal
+
anonymous visitor pass
Conceptually:
Browser request
│
├── authenticated session cookie
│
└── anonymous visitor-pass metadata
↓
Server
↓
Resolve authenticated principal
↓
Visitor pass also present?
↓
Yes
↓
Adopt anonymous conversations
↓
Continue original request
For example, the widget could carry the visitor pass separately from the actual authentication credential:
GET /conversations
Cookie: session=<authenticated-session>
X-Extra-Visitor-Pass: <anonymous-pass>
The exact transport/header name should be decided as part of the implementation.
The important semantic distinction is:
session cookie
↓
Who is making the request?
visitor pass
↓
Which anonymous history may need to be adopted?
The anonymous visitor pass should not become the primary authentication credential once an authenticated cookie is available.
Why Separate the Credentials
We should avoid relying on ambiguous credential precedence such as:
Authorization: Bearer <anonymous-pass>
Cookie: session=<authenticated-user>
and then depending on authentication middleware to choose the desired identity.
These represent two different concepts:
Authentication credential
→ authenticated account identity
History hand-off credential
→ anonymous identity being adopted
The architecture should make that distinction explicit.
The LLM, UI caller, or request payload must also not be able to arbitrarily choose the authenticated identity.
Expected Flow
Still Anonymous
visitor pass exists
↓
GET /conversations
↓
server sees no authenticated account session
↓
no adoption
↓
normal anonymous response
There should be no additional:
required just to determine that the visitor is still anonymous.
User Signs In
The host application establishes its normal session cookie.
The widget does not need to know that this happened.
host login
↓
browser now has authenticated cookie
Then:
next GET /conversations
↓
browser sends authenticated session cookie
+
visitor hand-off pass
↓
server resolves authenticated principal
↓
server adopts anonymous history
↓
GET /conversations continues
↓
response already includes merged conversations
The important ordering is:
authenticate
↓
adopt history
↓
execute history request
not:
GET history
↓
discover later that history should have been merged
Keep /auth/link
This issue does not necessarily require removing the existing /auth/link endpoint.
It may still be useful for:
- explicit identity-refresh flows;
- bearer/token authentication;
- external integrations;
- backwards compatibility.
The goal is specifically to avoid making repeated /auth/link probing the identity-change detection mechanism for zero-code cookie mode.
Application Boundary
The hand-off should happen at an appropriate authenticated application boundary.
Conceptually:
HTTP Request
↓
Authentication
↓
Principal
↓
AnonymousHistoryAdopter
↓
Route / ConversationService
The implementation should avoid duplicating adoption logic independently across multiple endpoints.
If history adoption becomes request middleware or another shared application-level component, it must remain explicit enough that:
- its side effects are understandable;
- errors are handled correctly;
- it does not unexpectedly run for unrelated authentication modes;
- the same anonymous pass is not repeatedly processed after adoption.
Failure Semantics
History hand-off is important, but failures should have clearly defined behavior.
Cases to consider:
Invalid / expired visitor pass
An authenticated user's normal request should not become permanently unusable because an old visitor-pass reference exists.
The system should determine whether to:
ignore + discard invalid hand-off state
or return a specific recoverable error.
The behavior must be explicit and tested.
Temporary infrastructure failure
If adoption fails because of a temporary server/database failure, we should not silently mark the pass as successfully consumed.
A future request may need to retry.
Already adopted pass
The operation must remain idempotent.
same pass
→ adoption already completed
→ no duplicate conversations
→ normal request continues
Security Considerations
The visitor pass must never allow one authenticated user to adopt another user's data without proper validation.
The server must continue validating:
visitor-pass authenticity
ownership/state
whether it was already consumed
target authenticated principal
The new hand-off mechanism must not weaken the authorization guarantees already enforced by /auth/link.
The visitor-pass transport must also not accidentally expose it through:
- logs;
- URLs/query strings;
- analytics;
- browser history.
Prefer a request header or another transport appropriate to the existing authentication architecture.
Remove Frontend Authentication Guessing Where Possible
Once a deterministic server-side mechanism exists, review whether the following cookie-mode complexity can be removed or simplified:
forceCheck
cookie snapshot tracking
identityCheckDirty
focus-based identity detection
visibilitychange-based identity detection
storage-event identity detection
Some of these mechanisms may still have value for other authentication modes.
Do not remove them mechanically.
The goal is to ensure that cookie-mode correctness does not depend on the frontend guessing authentication state.
Tests
1. Repeated anonymous history requests
visitor remains anonymous
listConversations()
listConversations()
listConversations()
Expected:
- normal anonymous history works
- no repeated POST /auth/link probing
- no unnecessary 401 identity probes
2. Zero-code login transition
anonymous visitor
↓
creates conversation
↓
host application signs user in
↓
NO reset()
NO refreshIdentity()
NO timeout/sleep
↓
next listConversations()
Expected ordering:
authenticated principal resolved
→ anonymous history adopted
→ conversations loaded
The response must include the previously anonymous conversation.
3. Immediate login transition
Login should work even if it occurs immediately after the previous anonymous request.
Correctness must not depend on an arbitrary elapsed time.
4. Pagination
list page 1
list page 2
list page 3
must not trigger repeated hand-off probes once identity state is already known/adoption is complete.
5. Already adopted pass
Repeated authenticated requests do not duplicate or reassign conversations.
6. Invalid visitor pass
An invalid/expired pass does not leak data and follows the explicitly defined failure policy.
7. Concurrent authenticated requests
If multiple requests arrive immediately after login, history must only be adopted once and all requests must observe a consistent final state.
The adoption path should be safe under concurrency.
8. Existing bearer mode
Existing token/bearer-based anonymous → account linking behavior must remain unchanged.
Acceptance Criteria
- Cookie-mode history hand-off does not rely on unconditional
forceCheck calls.
- Repeated anonymous history requests do not repeatedly call
/auth/link.
- No fixed timeout or sleep is required to detect login.
- Zero-code cookie authentication continues to work without
refreshIdentity().
- The first authenticated history request observes merged anonymous history.
- Authentication state is determined by the server rather than inferred through frontend timing.
- Anonymous hand-off identity is kept semantically separate from authenticated account credentials.
- The hand-off remains idempotent.
- Concurrent first requests after login cannot cause duplicate adoption.
- Existing bearer/token linking behavior is preserved.
- Invalid or expired visitor passes have explicitly defined behavior.
- Tests cover repeated anonymous requests, immediate login, pagination, concurrency, invalid passes, and the zero-code transition.
- Frontend cookie-mode identity-tracking complexity is simplified where it is no longer necessary.
Non-Goals
- Replacing the complete authentication architecture.
- Removing
/auth/link if it is still useful for other authentication modes.
- Adding polling or periodic session checks.
- Introducing a fixed retry delay.
- Requiring host applications to implement a new login callback for the standard cookie-mode flow.
Why This Matters
The widget should not need to guess whether the browser became authenticated.
In cookie mode, the server is already the authority that can answer that question reliably.
Moving the hand-off decision to the point where authenticated identity and anonymous history are both known gives us a simpler invariant:
If an authenticated request arrives with valid anonymous history,
adopt it before serving account-dependent data.
That removes repeated failed authentication probes and makes the anonymous → account transition deterministic rather than timing-dependent.
Refactor Cookie-Mode Anonymous History Hand-off to Avoid Repeated
/auth/linkProbesContext
Follow-up to #129.
#129 fixes the main anonymous → authenticated history hand-off flow in
host_token/ cookie authentication mode.The important behavior is now covered:
This works without requiring the host application to explicitly call:
However, the current implementation uses a forced identity check when conversation history is loaded.
Conceptually:
This guarantees correct ordering after login, but introduces an unnecessary network probe while the user is still anonymous.
Problem
While an anonymous visitor still has a stored visitor pass, every forced history check may attempt:
Repeated history operations may therefore look like:
The main functionality is correct, but we are using failed authentication requests as a mechanism for detecting whether the host application's authentication state has changed.
This has several disadvantages:
401responses;forceCheck, cookie snapshots, focus/visibility events, and retry state.Core Design Problem
In cookie authentication mode, the widget does not own the authenticated credential.
The browser may have an authenticated session cookie, potentially an
HttpOnlycookie, but the widget cannot reliably determine from JavaScript whether that cookie represents a valid authenticated session.This means the frontend does not have a reliable event equivalent to:
Trying to reconstruct that state through:
is inherently indirect.
The server, however, already has the authoritative information.
For every request it can determine:
Goal
Make anonymous-history adoption in cookie mode deterministic without requiring the widget to repeatedly probe
/auth/link.We should preserve all of the following:
/auth/linkrequests.refreshIdentity()call should be required for the normal zero-code cookie flow.Suggested Direction: Server-Side Opportunistic Hand-off
Instead of asking the frontend to determine whether authentication changed, consider letting the server perform the hand-off when both pieces of information are available on the same request:
Conceptually:
For example, the widget could carry the visitor pass separately from the actual authentication credential:
The exact transport/header name should be decided as part of the implementation.
The important semantic distinction is:
The anonymous visitor pass should not become the primary authentication credential once an authenticated cookie is available.
Why Separate the Credentials
We should avoid relying on ambiguous credential precedence such as:
and then depending on authentication middleware to choose the desired identity.
These represent two different concepts:
The architecture should make that distinction explicit.
The LLM, UI caller, or request payload must also not be able to arbitrarily choose the authenticated identity.
Expected Flow
Still Anonymous
There should be no additional:
required just to determine that the visitor is still anonymous.
User Signs In
The host application establishes its normal session cookie.
The widget does not need to know that this happened.
Then:
The important ordering is:
not:
Keep
/auth/linkThis issue does not necessarily require removing the existing
/auth/linkendpoint.It may still be useful for:
The goal is specifically to avoid making repeated
/auth/linkprobing the identity-change detection mechanism for zero-code cookie mode.Application Boundary
The hand-off should happen at an appropriate authenticated application boundary.
Conceptually:
The implementation should avoid duplicating adoption logic independently across multiple endpoints.
If history adoption becomes request middleware or another shared application-level component, it must remain explicit enough that:
Failure Semantics
History hand-off is important, but failures should have clearly defined behavior.
Cases to consider:
Invalid / expired visitor pass
An authenticated user's normal request should not become permanently unusable because an old visitor-pass reference exists.
The system should determine whether to:
or return a specific recoverable error.
The behavior must be explicit and tested.
Temporary infrastructure failure
If adoption fails because of a temporary server/database failure, we should not silently mark the pass as successfully consumed.
A future request may need to retry.
Already adopted pass
The operation must remain idempotent.
Security Considerations
The visitor pass must never allow one authenticated user to adopt another user's data without proper validation.
The server must continue validating:
The new hand-off mechanism must not weaken the authorization guarantees already enforced by
/auth/link.The visitor-pass transport must also not accidentally expose it through:
Prefer a request header or another transport appropriate to the existing authentication architecture.
Remove Frontend Authentication Guessing Where Possible
Once a deterministic server-side mechanism exists, review whether the following cookie-mode complexity can be removed or simplified:
Some of these mechanisms may still have value for other authentication modes.
Do not remove them mechanically.
The goal is to ensure that cookie-mode correctness does not depend on the frontend guessing authentication state.
Tests
1. Repeated anonymous history requests
2. Zero-code login transition
Expected ordering:
The response must include the previously anonymous conversation.
3. Immediate login transition
Login should work even if it occurs immediately after the previous anonymous request.
Correctness must not depend on an arbitrary elapsed time.
4. Pagination
must not trigger repeated hand-off probes once identity state is already known/adoption is complete.
5. Already adopted pass
Repeated authenticated requests do not duplicate or reassign conversations.
6. Invalid visitor pass
An invalid/expired pass does not leak data and follows the explicitly defined failure policy.
7. Concurrent authenticated requests
If multiple requests arrive immediately after login, history must only be adopted once and all requests must observe a consistent final state.
The adoption path should be safe under concurrency.
8. Existing bearer mode
Existing token/bearer-based anonymous → account linking behavior must remain unchanged.
Acceptance Criteria
forceCheckcalls./auth/link.refreshIdentity().Non-Goals
/auth/linkif it is still useful for other authentication modes.Why This Matters
The widget should not need to guess whether the browser became authenticated.
In cookie mode, the server is already the authority that can answer that question reliably.
Moving the hand-off decision to the point where authenticated identity and anonymous history are both known gives us a simpler invariant:
That removes repeated failed authentication probes and makes the anonymous → account transition deterministic rather than timing-dependent.