@@ -2712,10 +2712,18 @@ const chatBackgroundQueueKey = locals.create<ModelMessage[]>("chat.backgroundQue
27122712const chatInjectedInstructionsKey = locals . create < SystemModelMessage [ ] > (
27132713 "chat.injectedInstructions"
27142714) ;
2715- /** The turn that consumed the instructions lane, so a second read in the same turn still sees it. */
2716- const chatInstructionsConsumedTurnKey = locals . create < number | undefined > (
2717- "chat.injectedInstructionsConsumedTurn"
2718- ) ;
2715+ /**
2716+ * What a turn already consumed from the instructions lane, so a second
2717+ * `toStreamTextOptions()` call in the same turn sees the same blocks.
2718+ *
2719+ * Consumed blocks are moved here rather than left in the pending lane: leaving
2720+ * them there means an injection made during the consumed turn sits behind them,
2721+ * and clearing the lane on the next turn destroys both.
2722+ */
2723+ const chatInstructionsConsumedKey = locals . create < {
2724+ turn : number ;
2725+ blocks : SystemModelMessage [ ] ;
2726+ } > ( "chat.injectedInstructionsConsumed" ) ;
27192727
27202728/**
27212729 * Run-scoped pipe counter. Stored in locals so concurrent runs in the
@@ -4734,28 +4742,34 @@ function toStreamTextOptions(options?: ToStreamTextOptionsOptions): Record<strin
47344742 * caching, and the addition reads as a later amendment. A changed prefix does
47354743 * cost the first call its cache hit, on turns that actually injected.
47364744 */
4745+ /**
4746+ * Consumed once per turn, not once per read, and moved out of the lane rather
4747+ * than marked read in place.
4748+ *
4749+ * Per turn, because a `run()` that builds options twice (a cheap classifier
4750+ * pass and then the answer) has to see the injection in both, and draining on
4751+ * read hands it to whichever call ran first. Moved out, because blocks left in
4752+ * the lane sit in front of anything injected during the same turn, and
4753+ * clearing the lane on the next turn then destroys both. Outside a turn there
4754+ * is no turn to scope the stash to, so the lane drains on read there.
4755+ */
47374756 const injectedInstructions = locals . get ( chatInjectedInstructionsKey ) ;
4738- if ( injectedInstructions && injectedInstructions . length > 0 ) {
4739- /**
4740- * Consumed once per turn, not once per read. A `run()` that builds options
4741- * twice, a cheap classifier pass and then the answer, has to see the
4742- * injection in both: draining on read hands it to whichever call ran first
4743- * and drops it from the rest without saying so. Outside a turn there is no
4744- * turn to scope that to, so the lane drains on read there instead.
4745- */
4746- const currentTurn = locals . get ( chatTurnContextKey ) ?. turn ;
4747- const consumedTurn = locals . get ( chatInstructionsConsumedTurnKey ) ;
4748-
4749- let blocks : SystemModelMessage [ ] ;
4750- if ( currentTurn === undefined ) {
4751- blocks = injectedInstructions . splice ( 0 ) ;
4752- } else if ( consumedTurn !== undefined && consumedTurn !== currentTurn ) {
4753- injectedInstructions . length = 0 ;
4754- blocks = [ ] ;
4755- } else {
4756- locals . set ( chatInstructionsConsumedTurnKey , currentTurn ) ;
4757- blocks = injectedInstructions ;
4757+ const currentTurn = locals . get ( chatTurnContextKey ) ?. turn ;
4758+ const consumedThisTurn =
4759+ currentTurn === undefined ? undefined : locals . get ( chatInstructionsConsumedKey ) ;
4760+
4761+ let injectedBlocks : SystemModelMessage [ ] = [ ] ;
4762+ if ( consumedThisTurn && consumedThisTurn . turn === currentTurn ) {
4763+ injectedBlocks = consumedThisTurn . blocks ;
4764+ } else if ( injectedInstructions && injectedInstructions . length > 0 ) {
4765+ injectedBlocks = injectedInstructions . splice ( 0 ) ;
4766+ if ( currentTurn !== undefined ) {
4767+ locals . set ( chatInstructionsConsumedKey , { turn : currentTurn , blocks : injectedBlocks } ) ;
47584768 }
4769+ }
4770+
4771+ if ( injectedBlocks . length > 0 ) {
4772+ const blocks = injectedBlocks ;
47594773
47604774 const injectedText = blocks
47614775 . map ( ( block ) => ( typeof block . content === "string" ? block . content : "" ) )
0 commit comments