Anonymous users lose chat history across requests
## Summary
Anonymous users using the AGUI chat widget have no conversation history - `AiAssistantApiRunner::getMessageHistory()` always returns only the current user message (1 item). Each request generates a new `core.tempstore.private.owner` key because the owner is created inside the `StreamedResponse` callback, which executes *after* `kernel.response` has already persisted the session. The owner is never saved to persistent storage, so every request effectively starts fresh with a different TempStore identity.
## Steps to reproduce
1. Install and configure `agui` module with an AI assistant that has `allow_history: session`
2. Open the chat widget as an **anonymous** (not logged in) user
3. Send a first message — assistant responds correctly
4. Send a second message (follow-up question referencing the first)
5. Observe that the assistant has no memory of the first exchange — chat history passed to the LLM contains only 1 item (the latest user message)
## What is the current bug behavior?
`AiAssistantApiRunner::getMessageHistory()` always returns only the latest user message for anonymous users. Each request generates a different `core.tempstore.private.owner` because:
1. `setUserMessage()` is called inside the `StreamedResponse` callback
2. This triggers `PrivateTempStore::set()` which generates the owner key in the session bag
3. But `kernel.response` already called `session_write_close()` before the callback ran
4. The owner key is never persisted — next request generates a new one
5. `TempStore::get()` looks up data under the new owner and finds nothing
The net result: user and assistant messages are written to the database each request, but under a different owner key every time. They can never be retrieved.
## What is the expected correct behavior?
- The `core.tempstore.private.owner` should be generated and persisted from the very first request
- All subsequent requests should resolve to the same owner
- `getMessageHistory()` should return the full conversation (up to `history_context_length`)
- The LLM should receive conversation context enabling follow-up questions
## Root cause
`PrivateTempStore::set()` generates `core.tempstore.private.owner` in the session bag on first write for anonymous users. But since the session was already closed and written, this value exists only in memory for the duration of the callback and is lost.
For reference, `ai_chatbot`'s `DeepChatApi` controller seems to avoid this because it calls `setUserMessage()` and `process()` in the controller body *before* creating the `StreamedResponse` — the owner is generated while the session is still open.
## Fix
Move `setAssistant()` and `setUserMessage()` from the `StreamedResponse` callback into the **controller body (`chat()` method)**. This ensures `PrivateTempStore::set()` generates the owner key while the session is still active, so `kernel.response` persists it and sends the cookie.
Potential effects of setAssistant() and setUserMessage() in chat()? - No negative effects. These only set properties on the runner and write to TempStore. Moving them earlier seems to be actually the correct place because:
- setAssistant() generates the threadId (needed before response)
- setUserMessage() triggers TempStore::set() which generates the owner (needed for session persistence)
- Neither produces output or requires the streaming context
Additionally, capture and restore the owner in the callback. `startSession()` must be called in the callback (TempStore operations during `process()` require an active session), but on the first request it enters the "lazy" path which wipes `$_SESSION` (in `SessionManager::start`). The captured owner is restored afterward to maintain consistency.
```php
// In chat() - before creating StreamedResponse:
$this->aiAssistantApiRunner->setAssistant($assistant);
$this->aiAssistantApiRunner->setUserMessage(new UserMessage($messageText));
$tempStoreOwner = NULL;
if ($this->currentUser()->isAnonymous()) {
$tempStoreOwner = $request->getSession()->get('core.tempstore.private.owner');
}
return $this->createAgentStreamResponse($tempStoreOwner);
```
```php
// In the StreamedResponse callback - before process():
$this->aiAssistantApiRunner->startSession();
if ($tempStoreOwner && $this->currentUser()->isAnonymous()) {
$session = $this->requestStack->getCurrentRequest()->getSession();
if (!$session->has('core.tempstore.private.owner')) {
$session->set('core.tempstore.private.owner', $tempStoreOwner);
}
}
```
issue