@@ -18,6 +18,8 @@ import {
1818 applyOrchestrationEvents ,
1919 selectEnvironmentState ,
2020 selectProjectsAcrossEnvironments ,
21+ selectThreadByRef ,
22+ selectThreadExistsByRef ,
2123 setThreadBranch ,
2224 selectThreadsAcrossEnvironments ,
2325 syncServerReadModel ,
@@ -245,6 +247,128 @@ function makeEvent<T extends OrchestrationEvent["type"]>(
245247 } as Extract < OrchestrationEvent , { type : T } > ;
246248}
247249
250+ describe ( "thread selection memoization" , ( ) => {
251+ it ( "returns stable thread references for repeated reads of the same state" , ( ) => {
252+ const thread = makeThread ( {
253+ messages : [
254+ {
255+ id : MessageId . make ( "message-1" ) ,
256+ role : "user" ,
257+ text : "hello" ,
258+ createdAt : "2026-02-13T00:01:00.000Z" ,
259+ streaming : false ,
260+ } ,
261+ ] ,
262+ activities : [
263+ {
264+ id : EventId . make ( "activity-1" ) ,
265+ tone : "info" ,
266+ kind : "step" ,
267+ summary : "working" ,
268+ payload : { } ,
269+ turnId : TurnId . make ( "turn-1" ) ,
270+ createdAt : "2026-02-13T00:01:30.000Z" ,
271+ } ,
272+ ] ,
273+ proposedPlans : [
274+ {
275+ id : "plan-1" ,
276+ turnId : null ,
277+ planMarkdown : "plan" ,
278+ implementedAt : null ,
279+ implementationThreadId : null ,
280+ createdAt : "2026-02-13T00:02:00.000Z" ,
281+ updatedAt : "2026-02-13T00:02:00.000Z" ,
282+ } ,
283+ ] ,
284+ turnDiffSummaries : [
285+ {
286+ turnId : TurnId . make ( "turn-1" ) ,
287+ completedAt : "2026-02-13T00:03:00.000Z" ,
288+ files : [ ] ,
289+ } ,
290+ ] ,
291+ } ) ;
292+ const state = makeState ( thread ) ;
293+ const ref = scopeThreadRef ( thread . environmentId , thread . id ) ;
294+
295+ const first = selectThreadByRef ( state , ref ) ;
296+ const second = selectThreadByRef ( state , ref ) ;
297+
298+ expect ( first ) . toBeDefined ( ) ;
299+ expect ( second ) . toBe ( first ) ;
300+ expect ( second ?. messages ) . toBe ( first ?. messages ) ;
301+ expect ( second ?. activities ) . toBe ( first ?. activities ) ;
302+ expect ( second ?. proposedPlans ) . toBe ( first ?. proposedPlans ) ;
303+ expect ( second ?. turnDiffSummaries ) . toBe ( first ?. turnDiffSummaries ) ;
304+ } ) ;
305+
306+ it ( "reuses the derived thread when the app state wrapper changes but thread data does not" , ( ) => {
307+ const thread = makeThread ( {
308+ messages : [
309+ {
310+ id : MessageId . make ( "message-1" ) ,
311+ role : "assistant" ,
312+ text : "done" ,
313+ createdAt : "2026-02-13T00:01:00.000Z" ,
314+ streaming : false ,
315+ } ,
316+ ] ,
317+ } ) ;
318+ const state = makeState ( thread ) ;
319+ const ref = scopeThreadRef ( thread . environmentId , thread . id ) ;
320+ const wrappedState : AppState = {
321+ ...state ,
322+ environmentStateById : { ...state . environmentStateById } ,
323+ } ;
324+
325+ const first = selectThreadByRef ( state , ref ) ;
326+ const second = selectThreadByRef ( wrappedState , ref ) ;
327+
328+ expect ( second ) . toBe ( first ) ;
329+ } ) ;
330+
331+ it ( "updates the derived thread when the underlying thread data changes" , ( ) => {
332+ const thread = makeThread ( ) ;
333+ const ref = scopeThreadRef ( thread . environmentId , thread . id ) ;
334+ const firstState = makeState ( thread ) ;
335+ const secondState = makeState ( {
336+ ...thread ,
337+ messages : [
338+ {
339+ id : MessageId . make ( "message-2" ) ,
340+ role : "user" ,
341+ text : "new" ,
342+ createdAt : "2026-02-13T00:04:00.000Z" ,
343+ streaming : false ,
344+ } ,
345+ ] ,
346+ } ) ;
347+
348+ const first = selectThreadByRef ( firstState , ref ) ;
349+ const second = selectThreadByRef ( secondState , ref ) ;
350+
351+ expect ( second ) . not . toBe ( first ) ;
352+ expect ( second ?. messages ) . toHaveLength ( 1 ) ;
353+ expect ( second ?. messages [ 0 ] ?. text ) . toBe ( "new" ) ;
354+ } ) ;
355+
356+ it ( "checks thread existence without materializing the full thread" , ( ) => {
357+ const thread = makeThread ( ) ;
358+ const state = makeState ( thread ) ;
359+ const ref = scopeThreadRef ( thread . environmentId , thread . id ) ;
360+
361+ expect ( selectThreadExistsByRef ( state , ref ) ) . toBe ( true ) ;
362+ expect (
363+ selectThreadExistsByRef (
364+ state ,
365+ scopeThreadRef ( thread . environmentId , ThreadId . make ( "missing" ) ) ,
366+ ) ,
367+ ) . toBe ( false ) ;
368+ expect ( selectThreadExistsByRef ( state , null ) ) . toBe ( false ) ;
369+ } ) ;
370+ } ) ;
371+
248372function makeReadModelThread ( overrides : Partial < OrchestrationReadModel [ "threads" ] [ number ] > ) {
249373 return {
250374 id : ThreadId . make ( "thread-1" ) ,
0 commit comments