Skip to content

fix(ai): seed system+history via initialPrompts; let session manage state#350

Merged
dobrinyonkov merged 2 commits into
masterfrom
fix/ai-clear-history-system-role
Jun 22, 2026
Merged

fix(ai): seed system+history via initialPrompts; let session manage state#350
dobrinyonkov merged 2 commits into
masterfrom
fix/ai-clear-history-system-role

Conversation

@dobrinyonkov

Copy link
Copy Markdown
Contributor

Problem

Two related bugs trigger:

Failed to execute 'promptStreaming' on 'LanguageModel': The 'system' role message must be the first message of a session.

  1. After clicking Reset history, sending a new prompt errors out.
  2. On a normal multi-turn chat, the 3rd or later prompt errors out.

Root cause

The Chrome Prompt API session retains conversation history internally across promptStreaming calls. The previous code sent a fresh [system, ...history, user] messages array on every call. From the session's perspective, the system message wasn't first on subsequent turns — so the API rejected it.

Reference: https://developer.chrome.com/docs/ai/prompt-api — system role goes through initialPrompts at session creation; later prompt()/promptStreaming() calls take only the new user input.

Fix

Use the API the way it's meant to be used:

  • createSession accepts initialPrompts — seeded with system prompt + any prior user/assistant turns.
  • promptStreaming sends only the new user message string; session keeps history internally.
  • _loadHistory re-seeds the session after loading stored messages so the model has context on chat reopen.

Test

  • Send several prompts in a row → no error, context preserved.
  • Click Reset history, send a prompt → no error, fresh session.
  • Reopen the panel after history was stored → model remembers prior turns.
  • 383 unit tests pass.

…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.
@cla-assistant

cla-assistant Bot commented Jun 12, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@plamenivanov91 plamenivanov91 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. _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.

  1. 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.

  1. 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.

  1. 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.
@plamenivanov91 plamenivanov91 self-requested a review June 22, 2026 07:42
@dobrinyonkov dobrinyonkov merged commit ec1befd into master Jun 22, 2026
2 checks passed
dobrinyonkov added a commit that referenced this pull request Jun 24, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants