fix(runtime): don't persist session_start hook output as a session message#2533
Merged
dgageot merged 1 commit intodocker:mainfrom Apr 27, 2026
Conversation
…ssage session_start hook output (typically the AddEnvironmentInfo env block) was added to the session as a chat.MessageRoleSystem item via sess.AddMessage. Because session_start fires inside RunStream — after the caller has already appended the user's message — the system message landed AFTER the user's first turn. The user-message emitter at the top of RunStream reads messages[len-1] and surfaces it as the UserMessageEvent, so the env info leaked verbatim into the visible transcript on the user's first question. Fix: hold session_start AdditionalContext as transient extras for the duration of RunStream and thread it into sess.GetMessages alongside turn_start output on every iteration. The model still sees the env info on every model call; the persisted transcript and the user message tail stay clean. Same-shape contract as turn_start, which was already transient. - pkg/runtime/hooks.go: executeSessionStartHooks now returns []chat.Message instead of mutating the session. Both start-hook helpers share a small contextMessages converter so the bodies collapse to a single dispatchHook call each. - pkg/runtime/loop.go: capture session_start extras once at the top of RunStream; combine with per-iteration turn_start extras via slices.Concat when calling sess.GetMessages. - pkg/runtime/runtime_test.go: pin the regression with TestRunStream_AddEnvironmentInfo_DoesNotPolluteSession — asserts the session contains only user+assistant roles and that UserMessageEvent.Message is "hello", not the <env> block. Assisted-By: docker-agent
gtardif
approved these changes
Apr 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When an agent has
add_environment_info: true(or any other agent flag thatgets wired to
session_start), the env block leaked into the visibletranscript on the user's first question, looking like the user had typed it:
Root cause
executeSessionStartHookswas persisting the hook'sAdditionalContexttothe session via
sess.AddMessage(session.SystemMessage(...)). Becausesession_startfires insideRunStream— after the caller has alreadyappended the user's message — the system message landed AFTER the user's
first turn. The user-message emitter at the top of
RunStreamreadsmessages[len-1]and surfaces it as theUserMessageEvent, so the envblock was relayed verbatim to the UI.
Fix
Hold
session_startAdditionalContextas transient extras for theduration of
RunStreamand thread it intosess.GetMessagesalongsideturn_startoutput on every iteration. Same-shape contract asturn_start, which was already transient. The model still sees the envinfo on every model call; the persisted transcript and the user message
tail stay clean.
pkg/runtime/hooks.go:executeSessionStartHooksnow returns[]chat.Messageinstead of mutating the session. Both start-hookhelpers share a small
contextMessagesconverter so the bodiescollapse to a single
dispatchHookcall each.pkg/runtime/loop.go: capturesession_startextras once at the topof
RunStream; combine with per-iterationturn_startextras viaslices.Concatwhen callingsess.GetMessages.pkg/runtime/runtime_test.go: pin the regression withTestRunStream_AddEnvironmentInfo_DoesNotPolluteSession— asserts thesession contains only
user+assistantroles and thatUserMessageEvent.Messageis"hello", not the<env>block.Verification
Verified the regression test fails on the unfixed tree with exactly the
user-reported diff (
<env>block inUserMessageEvent.Message) and passeson the fixed tree.
mise lintis clean (0 issues, no offenses);mise testis green end-to-end.