@@ -719,4 +719,190 @@ it.layer(layerProjectionStoreTest)("CheckpointCaptureServiceV2", (it) => {
719719 assert . deepEqual ( [ ...projected . readyCheckpointOrdinals ] . toSorted ( ) , [ 0 , 1 ] ) ;
720720 } ) ,
721721 ) ;
722+
723+ it . effect ( "completes a side chat run with a missing checkpoint and never touches git" , ( ) =>
724+ Effect . gen ( function * ( ) {
725+ const projectionStore = yield * ProjectionStore . ProjectionStoreV2 ;
726+ const now = yield * DateTime . now ;
727+ const sideChatThreadId = ThreadId . make ( "thread:checkpoint-capture-side-chat" ) ;
728+ const sideChatRunId = RunId . make ( "run:checkpoint-capture-side-chat" ) ;
729+ const sideChatScopeId = CheckpointScopeId . make ( "scope:checkpoint-capture-side-chat" ) ;
730+ const sideChatRootNodeId = NodeId . make ( "node:checkpoint-capture-side-chat-root" ) ;
731+ const sideChatProviderThreadId = ProviderThreadId . make (
732+ "provider-thread:checkpoint-capture-side-chat" ,
733+ ) ;
734+ const event = { threadId : sideChatThreadId , occurredAt : now } ;
735+ const events : ReadonlyArray < OrchestrationV2DomainEvent > = [
736+ {
737+ ...event ,
738+ id : EventId . make ( "event:checkpoint-capture-side-chat:thread" ) ,
739+ type : "thread.created" ,
740+ payload : {
741+ createdBy : "user" ,
742+ creationSource : "web" ,
743+ id : sideChatThreadId ,
744+ projectId,
745+ title : "Side chat" ,
746+ providerInstanceId,
747+ modelSelection,
748+ runtimeMode : "approval-required" ,
749+ interactionMode : "default" ,
750+ branch : null ,
751+ worktreePath : "/repo" ,
752+ activeProviderThreadId : null ,
753+ lineage : {
754+ parentThreadId : threadId ,
755+ relationshipToParent : "fork" ,
756+ rootThreadId : threadId ,
757+ sideChat : true ,
758+ } ,
759+ forkedFrom : null ,
760+ createdAt : now ,
761+ updatedAt : now ,
762+ archivedAt : null ,
763+ settledOverride : null ,
764+ settledAt : null ,
765+ lastVisitedAt : null ,
766+ deletedAt : null ,
767+ } ,
768+ } ,
769+ {
770+ ...event ,
771+ id : EventId . make ( "event:checkpoint-capture-side-chat:provider-thread" ) ,
772+ type : "provider-thread.updated" ,
773+ driver,
774+ providerInstanceId,
775+ payload : {
776+ id : sideChatProviderThreadId ,
777+ driver,
778+ providerInstanceId,
779+ providerSessionId : null ,
780+ appThreadId : sideChatThreadId ,
781+ ownerNodeId : null ,
782+ nativeThreadRef : null ,
783+ nativeConversationHeadRef : null ,
784+ status : "idle" ,
785+ firstRunOrdinal : 1 ,
786+ lastRunOrdinal : 1 ,
787+ handoffIds : [ ] ,
788+ forkedFrom : null ,
789+ createdAt : now ,
790+ updatedAt : now ,
791+ } ,
792+ } ,
793+ {
794+ ...event ,
795+ id : EventId . make ( "event:checkpoint-capture-side-chat:run" ) ,
796+ type : "run.updated" ,
797+ runId : sideChatRunId ,
798+ payload : {
799+ id : sideChatRunId ,
800+ threadId : sideChatThreadId ,
801+ ordinal : 1 ,
802+ providerInstanceId,
803+ modelSelection,
804+ providerThreadId : sideChatProviderThreadId ,
805+ userMessageId : MessageId . make ( "message:checkpoint-capture-side-chat" ) ,
806+ rootNodeId : sideChatRootNodeId ,
807+ activeAttemptId : null ,
808+ status : "waiting" ,
809+ requestedAt : now ,
810+ startedAt : now ,
811+ completedAt : null ,
812+ checkpointId : null ,
813+ contextHandoffId : null ,
814+ } ,
815+ } ,
816+ {
817+ ...event ,
818+ id : EventId . make ( "event:checkpoint-capture-side-chat:node" ) ,
819+ type : "node.updated" ,
820+ runId : sideChatRunId ,
821+ nodeId : sideChatRootNodeId ,
822+ payload : {
823+ id : sideChatRootNodeId ,
824+ threadId : sideChatThreadId ,
825+ runId : sideChatRunId ,
826+ parentNodeId : null ,
827+ rootNodeId : sideChatRootNodeId ,
828+ kind : "root_turn" ,
829+ status : "waiting" ,
830+ countsForRun : true ,
831+ providerThreadId : sideChatProviderThreadId ,
832+ providerTurnId : null ,
833+ nativeItemRef : null ,
834+ runtimeRequestId : null ,
835+ checkpointScopeId : sideChatScopeId ,
836+ startedAt : now ,
837+ completedAt : null ,
838+ } ,
839+ } ,
840+ {
841+ ...event ,
842+ id : EventId . make ( "event:checkpoint-capture-side-chat:scope" ) ,
843+ type : "checkpoint-scope.created" ,
844+ runId : sideChatRunId ,
845+ nodeId : sideChatRootNodeId ,
846+ payload : {
847+ id : sideChatScopeId ,
848+ threadId : sideChatThreadId ,
849+ runId : sideChatRunId ,
850+ nodeId : sideChatRootNodeId ,
851+ parentScopeId : null ,
852+ providerThreadId : sideChatProviderThreadId ,
853+ kind : "root_run" ,
854+ ordinalWithinParent : 0 ,
855+ advancesAppRunCount : true ,
856+ cwd : "/repo" ,
857+ createdAt : now ,
858+ } ,
859+ } ,
860+ ] ;
861+ yield * Effect . forEach ( events , ( candidate ) => projectionStore . apply ( candidate ) ) ;
862+ const gitUnavailable = ( ) => Effect . die ( "side chats must not snapshot the shared worktree" ) ;
863+ const layerCapture = CheckpointCaptureService . layer . pipe (
864+ Layer . provide (
865+ Layer . mergeAll (
866+ IdAllocator . layer ,
867+ CheckpointService . layer . pipe (
868+ Layer . provide (
869+ Layer . mergeAll (
870+ IdAllocator . layer ,
871+ NodeCrypto . layer ,
872+ Layer . mock ( CheckpointStore . CheckpointStore ) ( {
873+ isGitRepository : gitUnavailable ,
874+ captureCheckpoint : gitUnavailable ,
875+ hasCheckpointRef : gitUnavailable ,
876+ } ) ,
877+ ) ,
878+ ) ,
879+ ) ,
880+ Layer . mock ( EventSink . EventSinkV2 ) ( {
881+ commitCommand : ( input ) =>
882+ Effect . forEach ( input . events , ( committed ) => projectionStore . apply ( committed ) ) . pipe (
883+ Effect . as ( { committed : true } as never ) ,
884+ Effect . orDie ,
885+ ) ,
886+ } ) ,
887+ ) ,
888+ ) ,
889+ ) ;
890+
891+ yield * CheckpointCaptureService . CheckpointCaptureServiceV2 . use ( ( service ) =>
892+ service . execute ( {
893+ threadId : sideChatThreadId ,
894+ runId : sideChatRunId ,
895+ scopeId : sideChatScopeId ,
896+ } ) ,
897+ ) . pipe ( Effect . provide ( layerCapture ) ) ;
898+
899+ const { checkpoints, runs } = yield * projectionStore . getThreadRecords ( sideChatThreadId , [
900+ "runs" ,
901+ "checkpoints" ,
902+ ] ) ;
903+ assert . equal ( runs [ 0 ] ?. status , "completed" ) ;
904+ const checkpoint = checkpoints . find ( ( candidate ) => candidate . id === runs [ 0 ] ?. checkpointId ) ;
905+ assert . equal ( checkpoint ?. status , "missing" ) ;
906+ } ) ,
907+ ) ;
722908} ) ;
0 commit comments