fix(ai): seed system+history via initialPrompts; let session manage state#350
Conversation
…tate Chrome Prompt API rejects 'system' role messages that aren't first in the session. Previously every promptStreaming call sent a fresh [system, ...history, user] array, which broke on the second turn since the session already had internal history. Switch to the API's intended model: - createSession accepts initialPrompts; seed with system prompt + any prior user/assistant turns. - promptStreaming sends only the new user message; session retains history internally. - _loadHistory re-seeds the session after loading stored messages so the model has context on chat reopen. Fixes 'system role message must be the first message of a session' errors after Reset history and on multi-turn conversations.
plamenivanov91
left a comment
There was a problem hiding this comment.
I've inspected and tested the code. It fixes the issue but the AI fond some issues.
The core approach (seeding history via initialPrompts and letting the session manage state) is sound. Two confirmed bugs need fixing before merge, plus a few lower-priority observations.
🚨 Mandatory
- _loadHistory never clears this._messages before appending
AIChat.js ~L1068
_addMessage always pushes to this._messages, and _loadHistory doesn't reset the array first. onTabActivated calls _loadHistory on every tab switch, so switching to the AI tab twice doubles every message in the array. _initializeSession then replays the bloated array as initialPrompts, feeding the model a duplicated conversation.
// Before the _addMessage loop in _loadHistory, add:
this._messages = [];
2. No session re-seed when navigating to a URL with no stored history
AIChat.js ~L1072
The destroy + _initializeSession block is inside if (messages.length > 0). Navigating from App A (has history) to App B (no history) skips the block entirely — the session retains App A's system prompt with App A's framework version, theme, and libraries.
// Move the re-seed block outside the messages.length guard:
if (this._sessionManager.hasActiveSession()) {
this._sessionManager.destroy();
await this._initializeSession();
}
if (messages.length > 0) {
// render messages...
}
💡 Nice to Have
3. No cap on initialPrompts replay
AIChat.js ~L229
All messages are replayed unconditionally. On a long conversation, LanguageModel.create() could be called with an initialPrompts array that hits or exceeds the session's inputQuota, leaving zero tokens for new input. Consider capping replay to the most recent N turns or checking tokensSoFar before pushing.
- handleCreateSession destroys the session before the new one is ready
main.js ~L311
promptAPISession is set to null before initPromptAPISession is called. If initPromptAPISession throws, promptAPISession stays null permanently — every subsequent prompt returns "No active session" until the user manually retries. Consider creating the new session first and only destroying the old one on success.
- Empty assistant placeholder replayed in initialPrompts mid-stream
AIChat.js ~L322
An { role: 'assistant', content: '' } placeholder is pushed to this._messages before streaming completes. onTabActivated has no _isStreaming guard, so switching tabs mid-stream triggers _loadHistory → _initializeSession, which replays the empty assistant turn into initialPrompts. The Chrome Prompt API may reject or behave oddly with blank-content turns. A simple fix: skip entries with empty content in the initialPrompts filter.
- Race condition in _loadHistory re-seed
AIChat.js ~L1086
Between destroy() and the await _initializeSession() resolving, hasActiveSession() is false. A concurrent message send hits the _handleSendMessage lazy-init path and fires a second _initializeSession() in parallel. Both race to the background, which destroys the first session to create the second. Low probability in practice but worth a mutex/guard flag.
- _loadHistory: clear _messages before repopulating so re-entry doesn't duplicate every turn (every tab switch was bloating the array). - _loadHistory: re-seed the session even when no stored history exists, so switching to a fresh app context refreshes the system prompt (framework version, theme, libraries) instead of keeping the old one. - _loadHistory: guard against re-entry while streaming or already re-seeding to avoid racing concurrent createSession calls. - _initializeSession: skip empty-content turns so the mid-stream assistant placeholder doesn't get replayed as a blank turn. - handleCreateSession (background): create the new LanguageModel session first and only destroy the old one on success — keeps the previous session usable if init throws.
Incorporates master's ec1befd (PR #350: fix(ai): seed system+history via initialPrompts; let session manage state). Master's hunks targeted code that has been refactored into PromptBuilder, PromptClient, and AssistantController on this branch; the conflicts were resolved by keeping the post-refactor shape and verifying that both behavior improvements from PR #350 are preserved in their new homes: - background/main.js handleCreateSession still creates the new LanguageModel session before destroying the prior one, so a failure during init leaves the previous session usable. - PromptBuilder.buildSeedMessages still skips empty assistant placeholders when replaying Conversation Memory, so an interrupted mid-stream slot never reseeds as a blank turn. Adds two tests exercising these behaviors at the right boundaries: - AssistantController.spec.js: empty assistant placeholders from Conversation Memory are not replayed into the session seed. - PromptClient.spec.js: a failed second createSession leaves hasActiveSession() === true so the prior background session can remain usable. See .scratch/ai-assistant-architecture-v1/issues/08-merge-master-preserve-behavior.md.
Problem
Two related bugs trigger:
Root cause
The Chrome Prompt API session retains conversation history internally across
promptStreamingcalls. The previous code sent a fresh[system, ...history, user]messages array on every call. From the session's perspective, thesystemmessage wasn't first on subsequent turns — so the API rejected it.Reference: https://developer.chrome.com/docs/ai/prompt-api — system role goes through
initialPromptsat session creation; laterprompt()/promptStreaming()calls take only the new user input.Fix
Use the API the way it's meant to be used:
createSessionacceptsinitialPrompts— seeded with system prompt + any prior user/assistant turns.promptStreamingsends only the new user message string; session keeps history internally._loadHistoryre-seeds the session after loading stored messages so the model has context on chat reopen.Test